diff --git a/.gitignore b/.gitignore index a0ff4d5b27..12ad3299ad 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ App_Data/TEMP/* src/Umbraco.Web.UI/[Cc]ss/* src/Umbraco.Web.UI/App_Code/* src/Umbraco.Web.UI/App_Data/* +src/Umbraco.Web.UI/data/* src/Umbraco.Tests/App_Data/* src/Umbraco.Web.UI/[Mm]edia/* src/Umbraco.Web.UI/[Mm]aster[Pp]ages/* diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index c8374bc2f7..347bde139e 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -28,7 +28,7 @@ - + @@ -52,14 +52,17 @@ + + + diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index 97e9ef3df2..a6b06d9964 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -26,7 +26,6 @@ not want this to happen as the alpha of the next major is, really, the next major already. --> - diff --git a/src/Umbraco.Core/Composing/ComponentCollection.cs b/src/Umbraco.Core/Composing/ComponentCollection.cs index 9b5319dc41..62b240f10f 100644 --- a/src/Umbraco.Core/Composing/ComponentCollection.cs +++ b/src/Umbraco.Core/Composing/ComponentCollection.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Umbraco.Core.Logging; @@ -43,8 +44,15 @@ namespace Umbraco.Core.Composing var componentType = component.GetType(); using (_logger.DebugDuration($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) { - component.Terminate(); - component.DisposeIfDisposable(); + try + { + component.Terminate(); + component.DisposeIfDisposable(); + } + catch (Exception ex) + { + _logger.Error(ex, "Error while terminating component {ComponentType}.", componentType.FullName); + } } } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IKeepAliveSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IKeepAliveSection.cs new file mode 100644 index 0000000000..3a0ad258c5 --- /dev/null +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IKeepAliveSection.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Core.Configuration.UmbracoSettings +{ + public interface IKeepAliveSection : IUmbracoConfigurationSection + { + bool DisableKeepAliveTask { get; } + string KeepAlivePingUrl { get; } + } +} diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs index 81d27e7bae..acd9c92588 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs @@ -15,5 +15,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings ILoggingSection Logging { get; } IWebRoutingSection WebRouting { get; } + + IKeepAliveSection KeepAlive { get; } } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/KeepAliveElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/KeepAliveElement.cs new file mode 100644 index 0000000000..89ba9be54d --- /dev/null +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/KeepAliveElement.cs @@ -0,0 +1,13 @@ +using System.Configuration; + +namespace Umbraco.Core.Configuration.UmbracoSettings +{ + internal class KeepAliveElement : ConfigurationElement, IKeepAliveSection + { + [ConfigurationProperty("disableKeepAliveTask", DefaultValue = "false")] + public bool DisableKeepAliveTask => (bool)base["disableKeepAliveTask"]; + + [ConfigurationProperty("keepAlivePingUrl", DefaultValue = "{umbracoApplicationUrl}/api/keepalive/ping")] + public string KeepAlivePingUrl => (string)base["keepAlivePingUrl"]; + } +} diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs index 7cf8096345..17430c81eb 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs @@ -19,10 +19,12 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("logging")] internal LoggingElement Logging => (LoggingElement)this["logging"]; - [ConfigurationProperty("web.routing")] internal WebRoutingElement WebRouting => (WebRoutingElement)this["web.routing"]; + [ConfigurationProperty("keepAlive")] + internal KeepAliveElement KeepAlive => (KeepAliveElement)this["keepAlive"]; + IContentSection IUmbracoSettingsSection.Content => Content; ISecuritySection IUmbracoSettingsSection.Security => Security; @@ -34,5 +36,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings ILoggingSection IUmbracoSettingsSection.Logging => Logging; IWebRoutingSection IUmbracoSettingsSection.WebRouting => WebRouting; + + IKeepAliveSection IUmbracoSettingsSection.KeepAlive => KeepAlive; } } diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 5b157307ab..fe5a82047a 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -19,6 +19,11 @@ namespace Umbraco.Core /// public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture(); + /// + /// Determines whether the content type varies by segment. + /// + public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment(); + /// /// Determines whether the content type is invariant. /// diff --git a/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs index 1c50a4b895..788310d7a1 100644 --- a/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs +++ b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs @@ -19,6 +19,8 @@ namespace Umbraco.Core.Manifest // '-content/foo', // hide for content type 'foo' // '+content/*', // show for all other content types // '+media/*', // show for all media types + // '-member/foo' // hide for member type 'foo' + // '+member/*' // show for all member types // '+role/admin' // show for admin users. Role based permissions will override others. // ] // }, @@ -56,6 +58,10 @@ namespace Umbraco.Core.Manifest partA = "media"; partB = media.ContentType.Alias; break; + case IMember member: + partA = "member"; + partB = member.ContentType.Alias; + break; default: return null; diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index ab3c40d917..dfd85b4cfc 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -187,7 +187,7 @@ namespace Umbraco.Core.Migrations.Upgrade To("{4759A294-9860-46BC-99F9-B4C975CAE580}"); To("{0BC866BC-0665-487A-9913-0290BD0169AD}"); To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}"); - + To("{EE288A91-531B-4995-8179-1D62D9AA3E2E}"); //FINAL } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs new file mode 100644 index 0000000000..75de01dd7f --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs @@ -0,0 +1,24 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 +{ + public class MissingContentVersionsIndexes : MigrationBase + { + public MissingContentVersionsIndexes(IMigrationContext context) : base(context) + { + } + + public override void Migrate() + { + Create + .Index("IX_" + ContentVersionDto.TableName + "_NodeId") + .OnTable(ContentVersionDto.TableName) + .OnColumn("nodeId") + .Ascending() + .OnColumn("current") + .Ascending() + .WithOptions().NonClustered() + .Do(); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs index 3c2c3deda4..4b203c128f 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs @@ -19,6 +19,7 @@ namespace Umbraco.Core.Persistence.Dtos [Column("nodeId")] [ForeignKey(typeof(ContentDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current")] public int NodeId { get; set; } [Column("versionDate")] // TODO: db rename to 'updateDate' @@ -30,7 +31,6 @@ namespace Umbraco.Core.Persistence.Dtos [NullSetting(NullSetting = NullSettings.Null)] public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero - // TODO: we need an index on this it is used almost always in querying and sorting [Column("current")] public bool Current { get; set; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 6385482686..e2c3d8c9b5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -16,7 +16,6 @@ using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; using Umbraco.Core.Services; -using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -506,7 +505,7 @@ AND umbracoNode.id <> @id", /// /// Corrects the property type variations for the given entity /// to make sure the property type variation is compatible with the - /// variation set on the entity itself. + /// variation set on the entity itself. /// /// Entity to correct properties for private void CorrectPropertyTypeVariations(IContentTypeComposition entity) @@ -754,7 +753,7 @@ AND umbracoNode.id <> @id", //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 + // if we want these SQL statements back, look into GIT history } } @@ -1033,7 +1032,7 @@ AND umbracoNode.id <> @id", //keep track of this node/lang to mark or unmark a culture as edited var editedLanguageVersions = new Dictionary<(int nodeId, int? langId), bool>(); - //keep track of which node to mark or unmark as edited + //keep track of which node to mark or unmark as edited var editedDocument = new Dictionary(); var nodeId = -1; var propertyTypeId = -1; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 98e51995f8..ccfa8209fb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Cache; -using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; @@ -245,6 +244,21 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return MapDtosToContent(Database.Fetch(sql), true); } + // TODO: This method needs to return a readonly version of IContent! The content returned + // from this method does not contain all of the data required to re-persist it and if that + // is attempted some odd things will occur. + // Either we create an IContentReadOnly (which ultimately we should for vNext so we can + // differentiate between methods that return entities that can be re-persisted or not), or + // in the meantime to not break API compatibility, we can add a property to IContentBase + // (or go further and have it on IUmbracoEntity): "IsReadOnly" and if that is true we throw + // an exception if that entity is passed to a Save method. + // Ideally we return "Slim" versions of content for all sorts of methods here and in ContentService. + // Perhaps another non-breaking alternative is to have new services like IContentServiceReadOnly + // which can return IContentReadOnly. + // We have the ability with `MapDtosToContent` to reduce the amount of data looked up for a + // content item. Ideally for paged data that populates list views, these would be ultra slim + // content items, there's no reason to populate those with really anything apart from property data, + // but until we do something like the above, we can't do that since it would be breaking and unclear. public override IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take) { var sql = GetBaseQuery(QueryType.Many, false) @@ -252,7 +266,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .OrderByDescending(x => x.Current) .AndByDescending(x => x.VersionDate); - return MapDtosToContent(Database.Fetch(sql), true, true).Skip(skip).Take(take); + return MapDtosToContent(Database.Fetch(sql), true, + // load bare minimum, need variants though since this is used to rollback with variants + false, false, false, true).Skip(skip).Take(take); } public override IContent GetVersion(int versionId) @@ -1076,7 +1092,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return base.ApplySystemOrdering(ref sql, ordering); } - private IEnumerable MapDtosToContent(List dtos, bool withCache = false, bool slim = false) + private IEnumerable MapDtosToContent(List dtos, + bool withCache = false, + bool loadProperties = true, + bool loadTemplates = true, + bool loadSchedule = true, + bool loadVariants = true) { var temps = new List>(); var contentTypes = new Dictionary(); @@ -1109,7 +1130,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); - if (!slim) + if (loadTemplates) { // need templates var templateId = dto.DocumentVersionDto.TemplateId; @@ -1134,49 +1155,71 @@ namespace Umbraco.Core.Persistence.Repositories.Implement temps.Add(temp); } - if (!slim) + Dictionary templates = null; + if (loadTemplates) { // load all required templates in 1 query, and index - var templates = _templateRepository.GetMany(templateIds.ToArray()) + templates = _templateRepository.GetMany(templateIds.ToArray()) .ToDictionary(x => x.Id, x => x); + } + IDictionary properties = null; + if (loadProperties) + { // load all properties for all documents from database in 1 query - indexed by version id - var properties = GetPropertyCollections(temps); - var schedule = GetContentSchedule(temps.Select(x => x.Content.Id).ToArray()); + properties = GetPropertyCollections(temps); + } - // assign templates and properties - foreach (var temp in temps) + var schedule = GetContentSchedule(temps.Select(x => x.Content.Id).ToArray()); + + // assign templates and properties + foreach (var temp in temps) + { + if (loadTemplates) { // set the template ID if it matches an existing template if (temp.Template1Id.HasValue && templates.ContainsKey(temp.Template1Id.Value)) temp.Content.TemplateId = temp.Template1Id; if (temp.Template2Id.HasValue && templates.ContainsKey(temp.Template2Id.Value)) temp.Content.PublishTemplateId = temp.Template2Id; + } + - // set properties + // set properties + if (loadProperties) + { if (properties.ContainsKey(temp.VersionId)) temp.Content.Properties = properties[temp.VersionId]; else throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'."); + } + if (loadSchedule) + { // load in the schedule if (schedule.TryGetValue(temp.Content.Id, out var s)) temp.Content.ContentSchedule = s; } + } - // set variations, if varying - temps = temps.Where(x => x.ContentType.VariesByCulture()).ToList(); - if (temps.Count > 0) + if (loadVariants) { - // load all variations for all documents from database, in one query - var contentVariations = GetContentVariations(temps); - var documentVariations = GetDocumentVariations(temps); - foreach (var temp in temps) - SetVariations(temp.Content, contentVariations, documentVariations); + // set variations, if varying + temps = temps.Where(x => x.ContentType.VariesByCulture()).ToList(); + if (temps.Count > 0) + { + // load all variations for all documents from database, in one query + var contentVariations = GetContentVariations(temps); + var documentVariations = GetDocumentVariations(temps); + foreach (var temp in temps) + SetVariations(temp.Content, contentVariations, documentVariations); + } } + - foreach(var c in content) + + foreach (var c in content) c.ResetDirtyProperties(false); // reset dirty initial properties (U4-1946) return content; diff --git a/src/Umbraco.Core/Properties/AssemblyInfo.cs b/src/Umbraco.Core/Properties/AssemblyInfo.cs index afd602cfc9..87e0732d47 100644 --- a/src/Umbraco.Core/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Core/Properties/AssemblyInfo.cs @@ -14,6 +14,7 @@ using System.Runtime.InteropServices; [assembly: InternalsVisibleTo("Umbraco.Web")] [assembly: InternalsVisibleTo("Umbraco.Web.UI")] [assembly: InternalsVisibleTo("Umbraco.Examine")] +[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.Embedded")] [assembly: InternalsVisibleTo("Umbraco.Tests")] [assembly: InternalsVisibleTo("Umbraco.Tests.Benchmarks")] diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index 31c446352e..82cab07b25 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -104,5 +104,17 @@ namespace Umbraco.Core.Services return found; } + + /// + /// Gets the concrete assigned permissions for the provided user and node + /// + /// + /// + /// + internal static string[] GetAssignedPermissions(this IUserService userService, IUser user, int nodeId) + { + var permissionCollection = userService.GetPermissions(user, nodeId); + return permissionCollection.SelectMany(c => c.AssignedPermissions).Distinct().ToArray(); + } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 1cdc56d253..801b4dfc3a 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -128,6 +128,7 @@ --> + @@ -359,6 +360,7 @@ + @@ -368,6 +370,7 @@ + diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 7eff1bddc2..0e0ee62139 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -49,7 +49,7 @@ - + 1.0.0-beta2-19324-01 runtime; build; native; contentfiles; analyzers; buildtransitive @@ -112,4 +112,4 @@ - + \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder.Embedded/ApiVersion.cs b/src/Umbraco.ModelsBuilder.Embedded/ApiVersion.cs new file mode 100644 index 0000000000..22347edd60 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/ApiVersion.cs @@ -0,0 +1,36 @@ +using System; +using System.Reflection; +using Semver; + +namespace Umbraco.ModelsBuilder.Embedded +{ + /// + /// Manages API version handshake between client and server. + /// + public class ApiVersion + { + /// + /// Initializes a new instance of the class. + /// + /// The currently executing version. + /// + internal ApiVersion(SemVersion executingVersion) + { + Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion)); + } + + private static SemVersion CurrentAssemblyVersion + => SemVersion.Parse(Assembly.GetExecutingAssembly().GetCustomAttribute().InformationalVersion); + + /// + /// Gets the currently executing API version. + /// + public static ApiVersion Current { get; } + = new ApiVersion(CurrentAssemblyVersion); + + /// + /// Gets the executing version of the API. + /// + public SemVersion Version { get; } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidator.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidator.cs new file mode 100644 index 0000000000..1fdb64c62a --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidator.cs @@ -0,0 +1,17 @@ +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.ModelsBuilder.Embedded.BackOffice +{ + /// + /// Used to validate the aliases for the content type when MB is enabled to ensure that + /// no illegal aliases are used + /// + // ReSharper disable once UnusedMember.Global - This is typed scanned + public class ContentTypeModelValidator : ContentTypeModelValidatorBase + { + public ContentTypeModelValidator(IModelsBuilderConfig config) : base(config) + { + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs new file mode 100644 index 0000000000..15ca2cca24 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web.Editors; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.ModelsBuilder.Embedded.BackOffice +{ + public abstract class ContentTypeModelValidatorBase : EditorValidator + where TModel : ContentTypeSave + where TProperty : PropertyTypeBasic + { + private readonly IModelsBuilderConfig _config; + + public ContentTypeModelValidatorBase(IModelsBuilderConfig config) + { + _config = config; + } + + protected override IEnumerable Validate(TModel model) + { + //don't do anything if we're not enabled + if (!_config.Enable) yield break; + + var properties = model.Groups.SelectMany(x => x.Properties) + .Where(x => x.Inherited == false) + .ToArray(); + + foreach (var prop in properties) + { + var propertyGroup = model.Groups.Single(x => x.Properties.Contains(prop)); + + if (model.Alias.ToLowerInvariant() == prop.Alias.ToLowerInvariant()) + yield return new ValidationResult(string.Format("With Models Builder enabled, you can't have a property with a the alias \"{0}\" when the content type alias is also \"{0}\".", prop.Alias), new[] + { + $"Groups[{model.Groups.IndexOf(propertyGroup)}].Properties[{propertyGroup.Properties.IndexOf(prop)}].Alias" + }); + + //we need to return the field name with an index so it's wired up correctly + var groupIndex = model.Groups.IndexOf(propertyGroup); + var propertyIndex = propertyGroup.Properties.IndexOf(prop); + + var validationResult = ValidateProperty(prop, groupIndex, propertyIndex); + if (validationResult != null) + yield return validationResult; + } + } + + private ValidationResult ValidateProperty(PropertyTypeBasic property, int groupIndex, int propertyIndex) + { + //don't let them match any properties or methods in IPublishedContent + //TODO: There are probably more! + var reservedProperties = typeof(IPublishedContent).GetProperties().Select(x => x.Name).ToArray(); + var reservedMethods = typeof(IPublishedContent).GetMethods().Select(x => x.Name).ToArray(); + + var alias = property.Alias; + + if (reservedProperties.InvariantContains(alias) || reservedMethods.InvariantContains(alias)) + return new ValidationResult( + $"The alias {alias} is a reserved term and cannot be used", new[] + { + $"Groups[{groupIndex}].Properties[{propertyIndex}].Alias" + }); + + return null; + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs new file mode 100644 index 0000000000..25ddc838e8 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs @@ -0,0 +1,61 @@ +using System.Text; +using Umbraco.ModelsBuilder.Embedded.Configuration; + +namespace Umbraco.ModelsBuilder.Embedded.BackOffice +{ + internal class DashboardReport + { + private readonly IModelsBuilderConfig _config; + private readonly OutOfDateModelsStatus _outOfDateModels; + private readonly ModelsGenerationError _mbErrors; + + public DashboardReport(IModelsBuilderConfig config, OutOfDateModelsStatus outOfDateModels, ModelsGenerationError mbErrors) + { + _config = config; + _outOfDateModels = outOfDateModels; + _mbErrors = mbErrors; + } + + public bool CanGenerate() => _config.ModelsMode.SupportsExplicitGeneration(); + + public bool AreModelsOutOfDate() => _outOfDateModels.IsOutOfDate; + + public string LastError() => _mbErrors.GetLastError(); + + public string Text() + { + if (!_config.Enable) + return "Version: " + ApiVersion.Current.Version + "
 
ModelsBuilder is disabled
(the .Enable key is missing, or its value is not 'true')."; + + var sb = new StringBuilder(); + + sb.Append("Version: "); + sb.Append(ApiVersion.Current.Version); + sb.Append("
 
"); + + sb.Append("ModelsBuilder is enabled, with the following configuration:"); + + sb.Append("
    "); + + sb.Append("
  • The models factory is "); + sb.Append(_config.EnableFactory || _config.ModelsMode == ModelsMode.PureLive + ? "enabled" + : "not enabled. Umbraco will not use models"); + sb.Append(".
  • "); + + sb.Append(_config.ModelsMode != ModelsMode.Nothing + ? $"
  • {_config.ModelsMode} models are enabled.
  • " + : "
  • No models mode is specified: models will not be generated.
  • "); + + sb.Append($"
  • Models namespace is {_config.ModelsNamespace}.
  • "); + + sb.Append("
  • Tracking of out-of-date models is "); + sb.Append(_config.FlagOutOfDateModels ? "enabled" : "not enabled"); + sb.Append(".
  • "); + + sb.Append("
"); + + return sb.ToString(); + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MediaTypeModelValidator.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MediaTypeModelValidator.cs new file mode 100644 index 0000000000..9dc1ea6c20 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MediaTypeModelValidator.cs @@ -0,0 +1,17 @@ +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.ModelsBuilder.Embedded.BackOffice +{ + /// + /// Used to validate the aliases for the content type when MB is enabled to ensure that + /// no illegal aliases are used + /// + // ReSharper disable once UnusedMember.Global - This is typed scanned + public class MediaTypeModelValidator : ContentTypeModelValidatorBase + { + public MediaTypeModelValidator(IModelsBuilderConfig config) : base(config) + { + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MemberTypeModelValidator.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MemberTypeModelValidator.cs new file mode 100644 index 0000000000..8d0a98eeab --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MemberTypeModelValidator.cs @@ -0,0 +1,17 @@ +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.ModelsBuilder.Embedded.BackOffice +{ + /// + /// Used to validate the aliases for the content type when MB is enabled to ensure that + /// no illegal aliases are used + /// + // ReSharper disable once UnusedMember.Global - This is typed scanned + public class MemberTypeModelValidator : ContentTypeModelValidatorBase + { + public MemberTypeModelValidator(IModelsBuilderConfig config) : base(config) + { + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs new file mode 100644 index 0000000000..1d9de265e9 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs @@ -0,0 +1,146 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Runtime.Serialization; +using System.Web.Hosting; +using Umbraco.Core.Exceptions; +using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web.Editors; +using Umbraco.Web.WebApi.Filters; + +namespace Umbraco.ModelsBuilder.Embedded.BackOffice +{ + /// + /// API controller for use in the Umbraco back office with Angular resources + /// + /// + /// We've created a different controller for the backoffice/angular specifically this is to ensure that the + /// correct CSRF security is adhered to for angular and it also ensures that this controller is not subseptipal to + /// global WebApi formatters being changed since this is always forced to only return Angular JSON Specific formats. + /// + [UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)] + public class ModelsBuilderDashboardController : UmbracoAuthorizedJsonController + { + private readonly IModelsBuilderConfig _config; + private readonly ModelsGenerator _modelGenerator; + private readonly OutOfDateModelsStatus _outOfDateModels; + private readonly ModelsGenerationError _mbErrors; + private readonly DashboardReport _dashboardReport; + + public ModelsBuilderDashboardController(IModelsBuilderConfig config, ModelsGenerator modelsGenerator, OutOfDateModelsStatus outOfDateModels, ModelsGenerationError mbErrors) + { + //_umbracoServices = umbracoServices; + _config = config; + _modelGenerator = modelsGenerator; + _outOfDateModels = outOfDateModels; + _mbErrors = mbErrors; + _dashboardReport = new DashboardReport(config, outOfDateModels, mbErrors); + } + + // invoked by the dashboard + // requires that the user is logged into the backoffice and has access to the settings section + // beware! the name of the method appears in modelsbuilder.controller.js + [System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers! + public HttpResponseMessage BuildModels() + { + try + { + var config = _config; + + if (!config.ModelsMode.SupportsExplicitGeneration()) + { + var result2 = new BuildResult { Success = false, Message = "Models generation is not enabled." }; + return Request.CreateResponse(HttpStatusCode.OK, result2, Configuration.Formatters.JsonFormatter); + } + + var bin = HostingEnvironment.MapPath("~/bin"); + if (bin == null) + throw new PanicException("bin is null."); + + // EnableDllModels will recycle the app domain - but this request will end properly + _modelGenerator.GenerateModels(); + _mbErrors.Clear(); + } + catch (Exception e) + { + _mbErrors.Report("Failed to build models.", e); + } + + return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter); + } + + // invoked by the back-office + // requires that the user is logged into the backoffice and has access to the settings section + [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! + public HttpResponseMessage GetModelsOutOfDateStatus() + { + var status = _outOfDateModels.IsEnabled + ? _outOfDateModels.IsOutOfDate + ? new OutOfDateStatus { Status = OutOfDateType.OutOfDate } + : new OutOfDateStatus { Status = OutOfDateType.Current } + : new OutOfDateStatus { Status = OutOfDateType.Unknown }; + + return Request.CreateResponse(HttpStatusCode.OK, status, Configuration.Formatters.JsonFormatter); + } + + // invoked by the back-office + // requires that the user is logged into the backoffice and has access to the settings section + // beware! the name of the method appears in modelsbuilder.controller.js + [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! + public HttpResponseMessage GetDashboard() + { + return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter); + } + + private Dashboard GetDashboardResult() + { + return new Dashboard + { + Enable = _config.Enable, + Text = _dashboardReport.Text(), + CanGenerate = _dashboardReport.CanGenerate(), + OutOfDateModels = _dashboardReport.AreModelsOutOfDate(), + LastError = _dashboardReport.LastError(), + }; + } + + [DataContract] + internal class BuildResult + { + [DataMember(Name = "success")] + public bool Success; + [DataMember(Name = "message")] + public string Message; + } + + [DataContract] + internal class Dashboard + { + [DataMember(Name = "enable")] + public bool Enable; + [DataMember(Name = "text")] + public string Text; + [DataMember(Name = "canGenerate")] + public bool CanGenerate; + [DataMember(Name = "outOfDateModels")] + public bool OutOfDateModels; + [DataMember(Name = "lastError")] + public string LastError; + } + + internal enum OutOfDateType + { + OutOfDate, + Current, + Unknown = 100 + } + + [DataContract] + internal class OutOfDateStatus + { + [DataMember(Name = "status")] + public OutOfDateType Status { get; set; } + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Building/Builder.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs similarity index 54% rename from src/Umbraco.ModelsBuilder/Building/Builder.cs rename to src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs index acfa402355..ffd56d4312 100644 --- a/src/Umbraco.ModelsBuilder/Building/Builder.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs @@ -1,14 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; +using Umbraco.ModelsBuilder.Embedded.Configuration; -namespace Umbraco.ModelsBuilder.Building +namespace Umbraco.ModelsBuilder.Embedded.Building { // NOTE // The idea was to have different types of builder, because I wanted to experiment with @@ -22,10 +17,10 @@ namespace Umbraco.ModelsBuilder.Building /// internal abstract class Builder { + private readonly IList _typeModels; protected Dictionary ModelsMap { get; } = new Dictionary(); - protected ParseResult ParseResult { get; } // the list of assemblies that will be 'using' by default protected readonly IList TypesUsing = new List @@ -37,8 +32,7 @@ namespace Umbraco.ModelsBuilder.Building "Umbraco.Core.Models", "Umbraco.Core.Models.PublishedContent", "Umbraco.Web", - "Umbraco.ModelsBuilder", - "Umbraco.ModelsBuilder.Umbraco", + "Umbraco.ModelsBuilder.Embedded" }; /// @@ -55,10 +49,10 @@ namespace Umbraco.ModelsBuilder.Building /// /// Gets the list of models to generate. /// - /// The models to generate, ie those that are not ignored. + /// The models to generate public IEnumerable GetModelsToGenerate() { - return _typeModels.Where(x => !x.IsContentIgnored); + return _typeModels; } /// @@ -67,90 +61,39 @@ namespace Umbraco.ModelsBuilder.Building /// Includes those that are ignored. internal IList TypeModels => _typeModels; - /// - /// Initializes a new instance of the class with a list of models to generate - /// and the result of code parsing. - /// - /// The list of models to generate. - /// The result of code parsing. - protected Builder(IList typeModels, ParseResult parseResult) - { - _typeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels)); - ParseResult = parseResult ?? throw new ArgumentNullException(nameof(parseResult)); - - Prepare(); - } - /// /// Initializes a new instance of the class with a list of models to generate, /// the result of code parsing, and a models namespace. /// /// The list of models to generate. - /// The result of code parsing. /// The models namespace. - protected Builder(IList typeModels, ParseResult parseResult, string modelsNamespace) - : this(typeModels, parseResult) + protected Builder(IModelsBuilderConfig config, IList typeModels) { + _typeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels)); + + Config = config ?? throw new ArgumentNullException(nameof(config)); + // can be null or empty, we'll manage - ModelsNamespace = modelsNamespace; + ModelsNamespace = Config.ModelsNamespace; + + // but we want it to prepare + Prepare(); } // for unit tests only protected Builder() { } + protected IModelsBuilderConfig Config { get; } + /// /// Prepares generation by processing the result of code parsing. /// - /// - /// Preparation includes figuring out from the existing code which models or properties should - /// be ignored or renamed, etc. -- anything that comes from the attributes in the existing code. - /// private void Prepare() { - var pureLive = UmbracoConfig.For.ModelsBuilder().ModelsMode == ModelsMode.PureLive; + TypeModel.MapModelTypes(_typeModels, ModelsNamespace); - // mark IsContentIgnored models that we discovered should be ignored - // then propagate / ignore children of ignored contents - // ignore content = don't generate a class for it, don't generate children - foreach (var typeModel in _typeModels.Where(x => ParseResult.IsIgnored(x.Alias))) - typeModel.IsContentIgnored = true; - foreach (var typeModel in _typeModels.Where(x => !x.IsContentIgnored && x.EnumerateBaseTypes().Any(xx => xx.IsContentIgnored))) - typeModel.IsContentIgnored = true; - - // handle model renames - foreach (var typeModel in _typeModels.Where(x => ParseResult.IsContentRenamed(x.Alias))) - { - typeModel.ClrName = ParseResult.ContentClrName(typeModel.Alias); - typeModel.IsRenamed = true; - ModelsMap[typeModel.Alias] = typeModel.ClrName; - } - - // handle implement - foreach (var typeModel in _typeModels.Where(x => ParseResult.HasContentImplement(x.Alias))) - { - typeModel.HasImplement = true; - } - - // mark OmitBase models that we discovered already have a base class - foreach (var typeModel in _typeModels.Where(x => ParseResult.HasContentBase(ParseResult.ContentClrName(x.Alias) ?? x.ClrName))) - typeModel.HasBase = true; - - foreach (var typeModel in _typeModels) - { - // mark IsRemoved properties that we discovered should be ignored - // ie is marked as ignored on type, or on any parent type - var tm = typeModel; - foreach (var property in typeModel.Properties - .Where(property => tm.EnumerateBaseTypes(true).Any(x => ParseResult.IsPropertyIgnored(ParseResult.ContentClrName(x.Alias) ?? x.ClrName, property.Alias)))) - { - property.IsIgnored = true; - } - - // handle property renames - foreach (var property in typeModel.Properties) - property.ClrName = ParseResult.PropertyClrName(ParseResult.ContentClrName(typeModel.Alias) ?? typeModel.ClrName, property.Alias) ?? property.ClrName; - } + var pureLive = Config.ModelsMode == ModelsMode.PureLive; // for the first two of these two tests, // always throw, even in purelive: cannot happen unless ppl start fidling with attributes to rename @@ -158,22 +101,22 @@ namespace Umbraco.ModelsBuilder.Building // for the last one, don't throw in purelive, see comment // ensure we have no duplicates type names - foreach (var xx in _typeModels.Where(x => !x.IsContentIgnored).GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) + foreach (var xx in _typeModels.GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) throw new InvalidOperationException($"Type name \"{xx.Key}\" is used" + $" for types with alias {string.Join(", ", xx.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Names have to be unique." + " Consider using an attribute to assign different names to conflicting types."); // ensure we have no duplicates property names - foreach (var typeModel in _typeModels.Where(x => !x.IsContentIgnored)) - foreach (var xx in typeModel.Properties.Where(x => !x.IsIgnored).GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) + foreach (var typeModel in _typeModels) + foreach (var xx in typeModel.Properties.GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) throw new InvalidOperationException($"Property name \"{xx.Key}\" in type {typeModel.ItemType}:\"{typeModel.Alias}\"" + $" is used for properties with alias {string.Join(", ", xx.Select(x => "\"" + x.Alias + "\""))}. Names have to be unique." + " Consider using an attribute to assign different names to conflicting properties."); // ensure content & property type don't have identical name (csharp hates it) - foreach (var typeModel in _typeModels.Where(x => !x.IsContentIgnored)) + foreach (var typeModel in _typeModels) { - foreach (var xx in typeModel.Properties.Where(x => !x.IsIgnored && x.ClrName == typeModel.ClrName)) + foreach (var xx in typeModel.Properties.Where(x => x.ClrName == typeModel.ClrName)) { if (!pureLive) throw new InvalidOperationException($"The model class for content type with alias \"{typeModel.Alias}\" is named \"{xx.ClrName}\"." @@ -204,7 +147,7 @@ namespace Umbraco.ModelsBuilder.Building // collect all the (non-removed) types implemented at parent level // ie the parent content types and the mixins content types, recursively var parentImplems = new List(); - if (typeModel.BaseType != null && !typeModel.BaseType.IsContentIgnored) + if (typeModel.BaseType != null) TypeModel.CollectImplems(parentImplems, typeModel.BaseType); // interfaces we must declare we implement (initially empty) @@ -212,7 +155,6 @@ namespace Umbraco.ModelsBuilder.Building // and except those that are already declared at the parent level // in other words, DeclaringInterfaces is "local mixins" var declaring = typeModel.MixinTypes - .Where(x => !x.IsContentIgnored) .Except(parentImplems); typeModel.DeclaringInterfaces.AddRange(declaring); @@ -227,43 +169,16 @@ namespace Umbraco.ModelsBuilder.Building typeModel.ImplementingInterfaces.AddRange(mixinImplems.Except(parentImplems)); } - // register using types - foreach (var usingNamespace in ParseResult.UsingNamespaces) + // ensure elements don't inherit from non-elements + foreach (var typeModel in _typeModels.Where(x => x.IsElement)) { - if (!TypesUsing.Contains(usingNamespace)) - TypesUsing.Add(usingNamespace); + if (typeModel.BaseType != null && !typeModel.BaseType.IsElement) + throw new InvalidOperationException($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but its parent type '{typeModel.BaseType.Alias}' is not."); + + var errs = typeModel.MixinTypes.Where(x => !x.IsElement).ToList(); + if (errs.Count > 0) + throw new InvalidOperationException($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but it is composed of {string.Join(", ", errs.Select(x => "'" + x.Alias + "'"))} which {(errs.Count == 1 ? "is" : "are")} not."); } - - // discover static mixin methods - foreach (var typeModel in _typeModels) - typeModel.StaticMixinMethods.AddRange(ParseResult.StaticMixinMethods(typeModel.ClrName)); - - // handle ctor - foreach (var typeModel in _typeModels.Where(x => ParseResult.HasCtor(x.ClrName))) - typeModel.HasCtor = true; - } - - private SemanticModel _ambiguousSymbolsModel; - private int _ambiguousSymbolsPos; - - // internal for tests - internal void PrepareAmbiguousSymbols() - { - var codeBuilder = new StringBuilder(); - foreach (var t in TypesUsing) - codeBuilder.AppendFormat("using {0};\n", t); - - codeBuilder.AppendFormat("namespace {0}\n{{ }}\n", GetModelsNamespace()); - - var compiler = new Compiler(); - SyntaxTree[] trees; - var compilation = compiler.GetCompilation("MyCompilation", new Dictionary { { "code", codeBuilder.ToString() } }, out trees); - var tree = trees[0]; - _ambiguousSymbolsModel = compilation.GetSemanticModel(tree); - - var namespaceSyntax = tree.GetRoot().DescendantNodes().OfType().First(); - //var namespaceSymbol = model.GetDeclaredSymbol(namespaceSyntax); - _ambiguousSymbolsPos = namespaceSyntax.OpenBraceToken.SpanStart; } // looking for a simple symbol eg 'Umbraco' or 'String' @@ -273,20 +188,12 @@ namespace Umbraco.ModelsBuilder.Building // - 1 symbol is found BUT not matching (implicitely ambiguous) protected bool IsAmbiguousSymbol(string symbol, string match) { - if (_ambiguousSymbolsModel == null) - PrepareAmbiguousSymbols(); - if (_ambiguousSymbolsModel == null) - throw new Exception("Could not prepare ambiguous symbols."); - var symbols = _ambiguousSymbolsModel.LookupNamespacesAndTypes(_ambiguousSymbolsPos, null, symbol); + // cannot figure out is a symbol is ambiguous without Roslyn + // so... let's say everything is ambiguous - code won't be + // pretty but it'll work - if (symbols.Length > 1) return true; - if (symbols.Length == 0) return false; // what else? - - // only 1 - ensure it matches - var found = symbols[0].ToDisplayString(); - var pos = found.IndexOf('<'); // generic? - if (pos > 0) found = found.Substring(0, pos); // strip - return found != match; // and compare + // Essentially this means that a `global::` syntax will be output for the generated models + return true; } internal string ModelsNamespaceForTests; @@ -296,25 +203,18 @@ namespace Umbraco.ModelsBuilder.Building if (ModelsNamespaceForTests != null) return ModelsNamespaceForTests; - // code attribute overrides everything - if (ParseResult.HasModelsNamespace) - return ParseResult.ModelsNamespace; - // if builder was initialized with a namespace, use that one if (!string.IsNullOrWhiteSpace(ModelsNamespace)) return ModelsNamespace; - // default - // fixme - should NOT reference config here, should make ModelsNamespace mandatory - return UmbracoConfig.For.ModelsBuilder().ModelsNamespace; + // use configured else fallback to default + return string.IsNullOrWhiteSpace(Config.ModelsNamespace) + ? ModelsBuilderConfig.DefaultModelsNamespace + : Config.ModelsNamespace; } protected string GetModelsBaseClassName(TypeModel type) { - // code attribute overrides everything - if (ParseResult.HasModelsBaseClassName) - return ParseResult.ModelsBaseClassName; - // default return type.IsElement ? "PublishedElementModel" : "PublishedContentModel"; } diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs new file mode 100644 index 0000000000..8a3bc5a5b5 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs @@ -0,0 +1,54 @@ +using System.IO; +using System.Text; +using Umbraco.ModelsBuilder.Embedded.Configuration; + +namespace Umbraco.ModelsBuilder.Embedded.Building +{ + public class ModelsGenerator + { + private readonly UmbracoServices _umbracoService; + private readonly IModelsBuilderConfig _config; + private readonly OutOfDateModelsStatus _outOfDateModels; + + public ModelsGenerator(UmbracoServices umbracoService, IModelsBuilderConfig config, OutOfDateModelsStatus outOfDateModels) + { + _umbracoService = umbracoService; + _config = config; + _outOfDateModels = outOfDateModels; + } + + internal void GenerateModels() + { + if (!Directory.Exists(_config.ModelsDirectory)) + Directory.CreateDirectory(_config.ModelsDirectory); + + foreach (var file in Directory.GetFiles(_config.ModelsDirectory, "*.generated.cs")) + File.Delete(file); + + var typeModels = _umbracoService.GetAllTypes(); + + var builder = new TextBuilder(_config, typeModels); + + foreach (var typeModel in builder.GetModelsToGenerate()) + { + var sb = new StringBuilder(); + builder.Generate(sb, typeModel); + var filename = Path.Combine(_config.ModelsDirectory, typeModel.ClrName + ".generated.cs"); + File.WriteAllText(filename, sb.ToString()); + } + + // the idea was to calculate the current hash and to add it as an extra file to the compilation, + // in order to be able to detect whether a DLL is consistent with an environment - however the + // environment *might not* contain the local partial files, and thus it could be impossible to + // calculate the hash. So... maybe that's not a good idea after all? + /* + var currentHash = HashHelper.Hash(ourFiles, typeModels); + ourFiles["models.hash.cs"] = $@"using Umbraco.ModelsBuilder; +[assembly:ModelsBuilderAssembly(SourceHash = ""{currentHash}"")] +"; + */ + + _outOfDateModels.Clear(); + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Building/PropertyModel.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/PropertyModel.cs similarity index 89% rename from src/Umbraco.ModelsBuilder/Building/PropertyModel.cs rename to src/Umbraco.ModelsBuilder.Embedded/Building/PropertyModel.cs index 1595b3f888..af5445b175 100644 --- a/src/Umbraco.ModelsBuilder/Building/PropertyModel.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/PropertyModel.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Umbraco.ModelsBuilder.Building +namespace Umbraco.ModelsBuilder.Embedded.Building { /// /// Represents a model property. @@ -41,11 +41,6 @@ namespace Umbraco.ModelsBuilder.Building /// public string ClrTypeName; - /// - /// Gets a value indicating whether this property should be excluded from generation. - /// - public bool IsIgnored; - /// /// Gets the generation errors for the property. /// diff --git a/src/Umbraco.ModelsBuilder/Building/TextBuilder.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs similarity index 83% rename from src/Umbraco.ModelsBuilder/Building/TextBuilder.cs rename to src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs index 85ccd541b7..d1190a0374 100644 --- a/src/Umbraco.ModelsBuilder/Building/TextBuilder.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs @@ -3,11 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.ModelsBuilder.Configuration; +using Umbraco.ModelsBuilder.Embedded.Configuration; -namespace Umbraco.ModelsBuilder.Building +namespace Umbraco.ModelsBuilder.Embedded.Building { /// /// Implements a builder that works by writing text. @@ -19,20 +17,8 @@ namespace Umbraco.ModelsBuilder.Building /// and the result of code parsing. /// /// The list of models to generate. - /// The result of code parsing. - public TextBuilder(IList typeModels, ParseResult parseResult) - : base(typeModels, parseResult) - { } - - /// - /// Initializes a new instance of the class with a list of models to generate, - /// the result of code parsing, and a models namespace. - /// - /// The list of models to generate. - /// The result of code parsing. - /// The models namespace. - public TextBuilder(IList typeModels, ParseResult parseResult, string modelsNamespace) - : base(typeModels, parseResult, modelsNamespace) + public TextBuilder(IModelsBuilderConfig config, IList typeModels) + : base(config, typeModels) { } // internal for unit tests only @@ -97,6 +83,20 @@ namespace Umbraco.ModelsBuilder.Building TextHeaderWriter.WriteHeader(sb); } + // writes an attribute that identifies code generated by a tool + // (helps reduce warnings, tools such as FxCop use it) + // see https://github.com/zpqrtbnk/Zbu.ModelsBuilder/issues/107 + // see https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.generatedcodeattribute + // see https://blogs.msdn.microsoft.com/codeanalysis/2007/04/27/correct-usage-of-the-compilergeneratedattribute-and-the-generatedcodeattribute/ + // + // note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class." + // and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself. + // + private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) + { + sb.AppendFormat("{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder\", \"{1}\")]\n", tabs, ApiVersion.Current.Version); + } + private void WriteContentType(StringBuilder sb, TypeModel type) { string sep; @@ -104,11 +104,11 @@ namespace Umbraco.ModelsBuilder.Building if (type.IsMixin) { // write the interface declaration - sb.AppendFormat("\t// Mixin content Type {0} with alias \"{1}\"\n", type.Id, type.Alias); + sb.AppendFormat("\t// Mixin Content Type with alias \"{0}\"\n", type.Alias); if (!string.IsNullOrWhiteSpace(type.Name)) sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName); - var implements = type.BaseType == null || type.BaseType.IsContentIgnored + var implements = type.BaseType == null ? (type.HasBase ? null : (type.IsElement ? "PublishedElement" : "PublishedContent")) : type.BaseType.ClrName; if (implements != null) @@ -126,7 +126,7 @@ namespace Umbraco.ModelsBuilder.Building // write the properties - only the local (non-ignored) ones, we're an interface var more = false; - foreach (var prop in type.Properties.Where(x => !x.IsIgnored).OrderBy(x => x.ClrName)) + foreach (var prop in type.Properties.OrderBy(x => x.ClrName)) { if (more) sb.Append("\n"); more = true; @@ -137,8 +137,6 @@ namespace Umbraco.ModelsBuilder.Building } // write the class declaration - if (type.IsRenamed) - sb.AppendFormat("\t// Content Type {0} with alias \"{1}\"\n", type.Id, type.Alias); if (!string.IsNullOrWhiteSpace(type.Name)) sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); // cannot do it now. see note in ImplementContentTypeAttribute @@ -148,7 +146,7 @@ namespace Umbraco.ModelsBuilder.Building sb.AppendFormat("\tpublic partial class {0}", type.ClrName); var inherits = type.HasBase ? null // has its own base already - : (type.BaseType == null || type.BaseType.IsContentIgnored + : (type.BaseType == null ? GetModelsBaseClassName(type) : type.BaseType.ClrName); if (inherits != null) @@ -178,22 +176,25 @@ namespace Umbraco.ModelsBuilder.Building // as 'new' since parent has its own - or maybe not - disable warning sb.Append("\t\t// helpers\n"); sb.Append("#pragma warning disable 0109 // new is redundant\n"); + WriteGeneratedCodeAttribute(sb, "\t\t"); sb.AppendFormat("\t\tpublic new const string ModelTypeAlias = \"{0}\";\n", type.Alias); var itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme + WriteGeneratedCodeAttribute(sb, "\t\t"); sb.AppendFormat("\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n", itemType); - sb.Append("\t\tpublic new static PublishedContentType GetModelContentType()\n"); + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.Append("\t\tpublic new static IPublishedContentType GetModelContentType()\n"); sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias);\n"); - sb.AppendFormat("\t\tpublic static PublishedPropertyType GetModelPropertyType(Expression> selector)\n", + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.AppendFormat("\t\tpublic static IPublishedPropertyType GetModelPropertyType(Expression> selector)\n", type.ClrName); sb.Append("\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(), selector);\n"); sb.Append("#pragma warning restore 0109\n\n"); // write the ctor - if (!type.HasCtor) - sb.AppendFormat("\t\t// ctor\n\t\tpublic {0}(IPublished{1} content)\n\t\t\t: base(content)\n\t\t{{ }}\n\n", - type.ClrName, type.IsElement ? "Element" : "Content"); + sb.AppendFormat("\t\t// ctor\n\t\tpublic {0}(IPublished{1} content)\n\t\t\t: base(content)\n\t\t{{ }}\n\n", + type.ClrName, type.IsElement ? "Element" : "Content"); // write the properties sb.Append("\t\t// properties\n"); @@ -205,10 +206,10 @@ namespace Umbraco.ModelsBuilder.Building private void WriteContentTypeProperties(StringBuilder sb, TypeModel type) { - var staticMixinGetters = UmbracoConfig.For.ModelsBuilder().StaticMixinGetters; + var staticMixinGetters = true; // write the properties - foreach (var prop in type.Properties.Where(x => !x.IsIgnored).OrderBy(x => x.ClrName)) + foreach (var prop in type.Properties.OrderBy(x => x.ClrName)) WriteProperty(sb, type, prop, staticMixinGetters && type.IsMixin ? type.ClrName : null); // no need to write the parent properties since we inherit from the parent @@ -217,7 +218,7 @@ namespace Umbraco.ModelsBuilder.Building // write the mixins properties foreach (var mixinType in type.ImplementingInterfaces.OrderBy(x => x.ClrName)) - foreach (var prop in mixinType.Properties.Where(x => !x.IsIgnored).OrderBy(x => x.ClrName)) + foreach (var prop in mixinType.Properties.OrderBy(x => x.ClrName)) if (staticMixinGetters) WriteMixinProperty(sb, prop, mixinType.ClrName); else @@ -242,6 +243,7 @@ namespace Umbraco.ModelsBuilder.Building sb.Append("\t\t///\n"); } + WriteGeneratedCodeAttribute(sb, "\t\t"); sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); sb.Append("\t\tpublic "); @@ -256,7 +258,7 @@ namespace Umbraco.ModelsBuilder.Building private static string MixinStaticGetterName(string clrName) { - return string.Format(UmbracoConfig.For.ModelsBuilder().StaticMixinGetterPattern, clrName); + return string.Format("Get{0}", clrName); } private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string mixinClrName = null) @@ -300,6 +302,7 @@ namespace Umbraco.ModelsBuilder.Building sb.Append("\t\t///\n"); } + WriteGeneratedCodeAttribute(sb, "\t\t"); sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); if (mixinStatic) @@ -336,13 +339,14 @@ namespace Umbraco.ModelsBuilder.Building var mixinStaticGetterName = MixinStaticGetterName(property.ClrName); - if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return; + //if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return; sb.Append("\n"); if (!string.IsNullOrWhiteSpace(property.Name)) sb.AppendFormat("\t\t/// Static getter for {0}\n", XmlCommentString(property.Name)); + WriteGeneratedCodeAttribute(sb, "\t\t"); sb.Append("\t\tpublic static "); WriteClrType(sb, property.ClrTypeName); sb.AppendFormat(" {0}(I{1} that) => that.Value", @@ -397,6 +401,7 @@ namespace Umbraco.ModelsBuilder.Building if (!string.IsNullOrWhiteSpace(property.Name)) sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); + WriteGeneratedCodeAttribute(sb, "\t\t"); sb.Append("\t\t"); WriteClrType(sb, property.ClrTypeName); sb.AppendFormat(" {0} {{ get; }}\n", @@ -461,7 +466,7 @@ namespace Umbraco.ModelsBuilder.Building s = Regex.Replace(s, @"\{(.*)\}\[\*\]", m => ModelsMap[m.Groups[1].Value + "[]"]); // takes care eg of "System.Int32" vs. "int" - if (TypesMap.TryGetValue(s.ToLowerInvariant(), out string typeName)) + if (TypesMap.TryGetValue(s, out string typeName)) { sb.Append(typeName); return; @@ -481,6 +486,11 @@ namespace Umbraco.ModelsBuilder.Building typeName = typeName.Substring(p + 1); typeUsing = x; } + else if (x == ModelsNamespace) // that one is used by default + { + typeName = typeName.Substring(p + 1); + typeUsing = ModelsNamespace; + } } // nested types *after* using @@ -531,24 +541,24 @@ namespace Umbraco.ModelsBuilder.Building return s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' '); } - private static readonly IDictionary TypesMap = new Dictionary + private static readonly IDictionary TypesMap = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "system.int16", "short" }, - { "system.int32", "int" }, - { "system.int64", "long" }, - { "system.string", "string" }, - { "system.object", "object" }, - { "system.boolean", "bool" }, - { "system.void", "void" }, - { "system.char", "char" }, - { "system.byte", "byte" }, - { "system.uint16", "ushort" }, - { "system.uint32", "uint" }, - { "system.uint64", "ulong" }, - { "system.sbyte", "sbyte" }, - { "system.single", "float" }, - { "system.double", "double" }, - { "system.decimal", "decimal" } + { "System.Int16", "short" }, + { "System.Int32", "int" }, + { "System.Int64", "long" }, + { "System.String", "string" }, + { "System.Object", "object" }, + { "System.Boolean", "bool" }, + { "System.Void", "void" }, + { "System.Char", "char" }, + { "System.Byte", "byte" }, + { "System.UInt16", "ushort" }, + { "System.UInt32", "uint" }, + { "System.UInt64", "ulong" }, + { "System.SByte", "sbyte" }, + { "System.Single", "float" }, + { "System.Double", "double" }, + { "System.Decimal", "decimal" } }; } } diff --git a/src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/TextHeaderWriter.cs similarity index 89% rename from src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs rename to src/Umbraco.ModelsBuilder.Embedded/Building/TextHeaderWriter.cs index d165f03907..a93df97806 100644 --- a/src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/TextHeaderWriter.cs @@ -1,9 +1,8 @@ using System.Text; -using Umbraco.ModelsBuilder.Api; -namespace Umbraco.ModelsBuilder.Building +namespace Umbraco.ModelsBuilder.Embedded.Building { - public static class TextHeaderWriter + internal static class TextHeaderWriter { /// /// Outputs an "auto-generated" header to a string builder. diff --git a/src/Umbraco.ModelsBuilder/Building/TypeModel.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModel.cs similarity index 85% rename from src/Umbraco.ModelsBuilder/Building/TypeModel.cs rename to src/Umbraco.ModelsBuilder.Embedded/Building/TypeModel.cs index 5ada8e881c..95356cf3ff 100644 --- a/src/Umbraco.ModelsBuilder/Building/TypeModel.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModel.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Models.PublishedContent; -namespace Umbraco.ModelsBuilder.Building +namespace Umbraco.ModelsBuilder.Embedded.Building { /// /// Represents a model. @@ -76,10 +77,10 @@ namespace Umbraco.ModelsBuilder.Building /// public readonly List ImplementingInterfaces = new List(); - /// - /// Gets the list of existing static mixin method candidates. - /// - public readonly List StaticMixinMethods = new List(); + ///// + ///// Gets the list of existing static mixin method candidates. + ///// + //public readonly List StaticMixinMethods = new List(); //TODO: Do we need this? it isn't used /// /// Gets a value indicating whether this model has a base class. @@ -88,16 +89,6 @@ namespace Umbraco.ModelsBuilder.Building /// or because the existing user's code declares a base class for this model. public bool HasBase; - /// - /// Gets a value indicating whether this model has been renamed. - /// - public bool IsRenamed; - - /// - /// Gets a value indicating whether this model has [ImplementContentType] already. - /// - public bool HasImplement; - /// /// Gets a value indicating whether this model is used as a mixin by another model. /// @@ -108,16 +99,6 @@ namespace Umbraco.ModelsBuilder.Building /// public bool IsParent; - /// - /// Gets a value indicating whether this model should be excluded from generation. - /// - public bool IsContentIgnored; - - /// - /// Gets a value indicating whether the ctor is already defined in a partial. - /// - public bool HasCtor; - /// /// Gets a value indicating whether the type is an element. /// @@ -181,11 +162,11 @@ namespace Umbraco.ModelsBuilder.Building /// Includes the specified type. internal static void CollectImplems(ICollection types, TypeModel type) { - if (!type.IsContentIgnored && types.Contains(type) == false) + if (types.Contains(type) == false) types.Add(type); - if (type.BaseType != null && !type.BaseType.IsContentIgnored) + if (type.BaseType != null) CollectImplems(types, type.BaseType); - foreach (var mixin in type.MixinTypes.Where(x => !x.IsContentIgnored)) + foreach (var mixin in type.MixinTypes) CollectImplems(types, mixin); } @@ -204,5 +185,21 @@ namespace Umbraco.ModelsBuilder.Building typeModel = typeModel.BaseType; } } + + /// + /// Maps ModelType. + /// + public static void MapModelTypes(IList typeModels, string ns) + { + var hasNs = !string.IsNullOrWhiteSpace(ns); + var map = typeModels.ToDictionary(x => x.Alias, x => hasNs ? (ns + "." + x.ClrName) : x.ClrName); + foreach (var typeModel in typeModels) + { + foreach (var propertyModel in typeModel.Properties) + { + propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map); + } + } + } } } diff --git a/src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs similarity index 81% rename from src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs rename to src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs index c530cbbd6b..2f14bec875 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs @@ -1,18 +1,14 @@ using System.Collections.Generic; using System.Linq; -using Umbraco.ModelsBuilder.Building; -namespace Umbraco.ModelsBuilder.Umbraco +namespace Umbraco.ModelsBuilder.Embedded.Building { - class HashHelper + internal class TypeModelHasher { - public static string Hash(IDictionary ourFiles, IEnumerable typeModels) + public static string Hash(IEnumerable typeModels) { var hash = new HashCombiner(); - foreach (var kvp in ourFiles) - hash.Add(kvp.Key + "::" + kvp.Value); - // see Umbraco.ModelsBuilder.Umbraco.Application for what's important to hash // ie what comes from Umbraco (not computed by ModelsBuilder) and makes a difference @@ -39,6 +35,9 @@ namespace Umbraco.ModelsBuilder.Umbraco } } + // Include the MB version in the hash so that if the MB version changes, models are rebuilt + hash.Add(ApiVersion.Current.Version.ToString()); + return hash.GetCombinedHashCode(); } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs new file mode 100644 index 0000000000..c599785711 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs @@ -0,0 +1,30 @@ +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.ModelsBuilder.Embedded.BackOffice; +using Umbraco.Web.Features; + +namespace Umbraco.ModelsBuilder.Embedded.Compose +{ + /// + /// Special component used for when MB is disabled with the legacy MB is detected + /// + internal class DisabledModelsBuilderComponent : IComponent + { + private readonly UmbracoFeatures _features; + + public DisabledModelsBuilderComponent(UmbracoFeatures features) + { + _features = features; + } + + public void Initialize() + { + //disable the embedded dashboard controller + _features.Disabled.Controllers.Add(); + } + + public void Terminate() + { + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs new file mode 100644 index 0000000000..0e41c9ac62 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Web; +using System.Web.Mvc; +using System.Web.Routing; +using Umbraco.Core.Composing; +using Umbraco.Core.IO; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; +using Umbraco.ModelsBuilder.Embedded.BackOffice; +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web; +using Umbraco.Web.JavaScript; +using Umbraco.Web.Mvc; + +namespace Umbraco.ModelsBuilder.Embedded.Compose +{ + + internal class ModelsBuilderComponent : IComponent + { + + private readonly IModelsBuilderConfig _config; + private readonly LiveModelsProvider _liveModelsProvider; + private readonly OutOfDateModelsStatus _outOfDateModels; + + public ModelsBuilderComponent(IModelsBuilderConfig config, LiveModelsProvider liveModelsProvider, OutOfDateModelsStatus outOfDateModels) + { + _config = config; + _liveModelsProvider = liveModelsProvider; + _outOfDateModels = outOfDateModels; + } + + public void Initialize() + { + // always setup the dashboard + // note: UmbracoApiController instances are automatically registered + InstallServerVars(); + + ContentModelBinder.ModelBindingException += ContentModelBinder_ModelBindingException; + + if (_config.Enable) + FileService.SavingTemplate += FileService_SavingTemplate; + + if (_config.ModelsMode.IsLiveNotPure()) + _liveModelsProvider.Install(); + + if (_config.FlagOutOfDateModels) + _outOfDateModels.Install(); + } + + public void Terminate() + { } + + private void InstallServerVars() + { + // register our url - for the backoffice api + ServerVariablesParser.Parsing += (sender, serverVars) => + { + if (!serverVars.ContainsKey("umbracoUrls")) + throw new ArgumentException("Missing umbracoUrls."); + var umbracoUrlsObject = serverVars["umbracoUrls"]; + if (umbracoUrlsObject == null) + throw new ArgumentException("Null umbracoUrls"); + if (!(umbracoUrlsObject is Dictionary umbracoUrls)) + throw new ArgumentException("Invalid umbracoUrls"); + + if (!serverVars.ContainsKey("umbracoPlugins")) + throw new ArgumentException("Missing umbracoPlugins."); + if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) + throw new ArgumentException("Invalid umbracoPlugins"); + + if (HttpContext.Current == null) throw new InvalidOperationException("HttpContext is null"); + var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData())); + + umbracoUrls["modelsBuilderBaseUrl"] = urlHelper.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); + umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); + }; + } + + private Dictionary GetModelsBuilderSettings() + { + var settings = new Dictionary + { + {"enabled", _config.Enable} + }; + + return settings; + } + + /// + /// Used to check if a template is being created based on a document type, in this case we need to + /// ensure the template markup is correct based on the model name of the document type + /// + /// + /// + private void FileService_SavingTemplate(IFileService sender, Core.Events.SaveEventArgs e) + { + // don't do anything if the factory is not enabled + // because, no factory = no models (even if generation is enabled) + if (!_config.EnableFactory) return; + + // don't do anything if this special key is not found + if (!e.AdditionalData.ContainsKey("CreateTemplateForContentType")) return; + + // ensure we have the content type alias + if (!e.AdditionalData.ContainsKey("ContentTypeAlias")) + throw new InvalidOperationException("The additionalData key: ContentTypeAlias was not found"); + + foreach (var template in e.SavedEntities) + // if it is in fact a new entity (not been saved yet) and the "CreateTemplateForContentType" key + // is found, then it means a new template is being created based on the creation of a document type + if (!template.HasIdentity && string.IsNullOrWhiteSpace(template.Content)) + { + // ensure is safe and always pascal cased, per razor standard + // + this is how we get the default model name in Umbraco.ModelsBuilder.Umbraco.Application + var alias = e.AdditionalData["ContentTypeAlias"].ToString(); + var name = template.Name; // will be the name of the content type since we are creating + var className = UmbracoServices.GetClrName(name, alias); + + var modelNamespace = _config.ModelsNamespace; + + // we do not support configuring this at the moment, so just let Umbraco use its default value + //var modelNamespaceAlias = ...; + + var markup = ViewHelper.GetDefaultFileContent( + modelClassName: className, + modelNamespace: modelNamespace/*, + modelNamespaceAlias: modelNamespaceAlias*/); + + //set the template content to the new markup + template.Content = markup; + } + } + + private void ContentModelBinder_ModelBindingException(object sender, ContentModelBinder.ModelBindingArgs args) + { + var sourceAttr = args.SourceType.Assembly.GetCustomAttribute(); + var modelAttr = args.ModelType.Assembly.GetCustomAttribute(); + + // if source or model is not a ModelsBuider type... + if (sourceAttr == null || modelAttr == null) + { + // if neither are ModelsBuilder types, give up entirely + if (sourceAttr == null && modelAttr == null) + return; + + // else report, but better not restart (loops?) + args.Message.Append(" The "); + args.Message.Append(sourceAttr == null ? "view model" : "source"); + args.Message.Append(" is a ModelsBuilder type, but the "); + args.Message.Append(sourceAttr != null ? "view model" : "source"); + args.Message.Append(" is not. The application is in an unstable state and should be restarted."); + return; + } + + // both are ModelsBuilder types + var pureSource = sourceAttr.PureLive; + var pureModel = modelAttr.PureLive; + + if (sourceAttr.PureLive || modelAttr.PureLive) + if (pureSource == false || pureModel == false) + { + // only one is pure - report, but better not restart (loops?) + args.Message.Append(pureSource + ? " The content model is PureLive, but the view model is not." + : " The view model is PureLive, but the content model is not."); + args.Message.Append(" The application is in an unstable state and should be restarted."); + } + else + { + // both are pure - report, and if different versions, restart + // if same version... makes no sense... and better not restart (loops?) + var sourceVersion = args.SourceType.Assembly.GetName().Version; + var modelVersion = args.ModelType.Assembly.GetName().Version; + args.Message.Append(" Both view and content models are PureLive, with "); + args.Message.Append(sourceVersion == modelVersion + ? "same version. The application is in an unstable state and should be restarted." + : "different versions. The application is in an unstable state and is going to be restarted."); + args.Restart = sourceVersion != modelVersion; + } + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs new file mode 100644 index 0000000000..45c4de5d2a --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs @@ -0,0 +1,103 @@ +using System.Linq; +using System.Reflection; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Composing; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web; +using Umbraco.Web.PublishedCache.NuCache; +using Umbraco.Web.Features; + +namespace Umbraco.ModelsBuilder.Embedded.Compose +{ + + + [ComposeBefore(typeof(NuCacheComposer))] + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public sealed class ModelsBuilderComposer : ICoreComposer + { + public void Compose(Composition composition) + { + var isLegacyModelsBuilderInstalled = IsLegacyModelsBuilderInstalled(); + + + composition.Configs.Add(() => new ModelsBuilderConfig()); + + if (isLegacyModelsBuilderInstalled) + { + ComposeForLegacyModelsBuilder(composition); + return; + } + + composition.Components().Append(); + composition.Register(Lifetime.Singleton); + + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + + if (composition.Configs.ModelsBuilder().ModelsMode == ModelsMode.PureLive) + ComposeForLiveModels(composition); + else if (composition.Configs.ModelsBuilder().EnableFactory) + ComposeForDefaultModelsFactory(composition); + } + + private static bool IsLegacyModelsBuilderInstalled() + { + Assembly legacyMbAssembly = null; + try + { + legacyMbAssembly = Assembly.Load("Umbraco.ModelsBuilder"); + } + catch (System.Exception) + { + //swallow exception, DLL must not be there + } + + return legacyMbAssembly != null; + } + + private void ComposeForLegacyModelsBuilder(Composition composition) + { + composition.Logger.Info("ModelsBuilder.Embedded is disabled, the external ModelsBuilder was detected."); + composition.Components().Append(); + composition.Dashboards().Remove(); + } + + private void ComposeForDefaultModelsFactory(Composition composition) + { + composition.RegisterUnique(factory => + { + var typeLoader = factory.GetInstance(); + var types = typeLoader + .GetTypes() // element models + .Concat(typeLoader.GetTypes()); // content models + return new PublishedModelFactory(types); + }); + } + + private void ComposeForLiveModels(Composition composition) + { + composition.RegisterUnique(); + + // the following would add @using statement in every view so user's don't + // have to do it - however, then noone understands where the @using statement + // comes from, and it cannot be avoided / removed --- DISABLED + // + /* + // no need for @using in views + // note: + // we are NOT using the in-code attribute here, config is required + // because that would require parsing the code... and what if it changes? + // we can AddGlobalImport not sure we can remove one anyways + var modelsNamespace = Configuration.Config.ModelsNamespace; + if (string.IsNullOrWhiteSpace(modelsNamespace)) + modelsNamespace = Configuration.Config.DefaultModelsNamespace; + System.Web.WebPages.Razor.WebPageRazorHost.AddGlobalImport(modelsNamespace); + */ + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderInitializer.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderInitializer.cs new file mode 100644 index 0000000000..a86669b135 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderInitializer.cs @@ -0,0 +1,28 @@ +using System.Web; +using System.Web.Compilation; +using Umbraco.ModelsBuilder.Embedded.Compose; + +[assembly: PreApplicationStartMethod(typeof(ModelsBuilderInitializer), "Initialize")] + +namespace Umbraco.ModelsBuilder.Embedded.Compose +{ + public static class ModelsBuilderInitializer + { + public static void Initialize() + { + // for some reason, netstandard is missing from BuildManager.ReferencedAssemblies and yet, is part of + // the references that CSharpCompiler receives - in some cases eg when building views - but not when + // using BuildManager to build the PureLive models - where is it coming from? cannot figure it out + + // so... cheating here + + // this is equivalent to adding + // + // to web.config system.web/compilation/assemblies + + var netStandard = ReferencedAssemblies.GetNetStandardAssembly(); + if (netStandard != null) + BuildManager.AddReferencedAssembly(netStandard); + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/ConfigsExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/ConfigsExtensions.cs new file mode 100644 index 0000000000..d634547a49 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/ConfigsExtensions.cs @@ -0,0 +1,20 @@ +using Umbraco.Core.Configuration; +using Umbraco.ModelsBuilder.Embedded.Configuration; + +namespace Umbraco.ModelsBuilder.Embedded +{ + /// + /// Provides extension methods for the class. + /// + public static class ConfigsExtensions + { + /// + /// Gets the models builder configuration. + /// + /// Getting the models builder configuration freezes its state, + /// and any attempt at modifying the configuration using the Setup method + /// will be ignored. + public static IModelsBuilderConfig ModelsBuilder(this Configs configs) + => configs.GetConfig(); + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/Configuration/IModelsBuilderConfig.cs b/src/Umbraco.ModelsBuilder.Embedded/Configuration/IModelsBuilderConfig.cs new file mode 100644 index 0000000000..7e96aec60e --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/Configuration/IModelsBuilderConfig.cs @@ -0,0 +1,15 @@ +namespace Umbraco.ModelsBuilder.Embedded.Configuration +{ + public interface IModelsBuilderConfig + { + bool Enable { get; } + bool AcceptUnsafeModelsDirectory { get; } + int DebugLevel { get; } + bool EnableFactory { get; } + bool FlagOutOfDateModels { get; } + bool IsDebug { get; } + string ModelsDirectory { get; } + ModelsMode ModelsMode { get; } + string ModelsNamespace { get; } + } +} diff --git a/src/Umbraco.ModelsBuilder/Configuration/Config.cs b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs similarity index 54% rename from src/Umbraco.ModelsBuilder/Configuration/Config.cs rename to src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs index ebfe4be709..c6bccdcf87 100644 --- a/src/Umbraco.ModelsBuilder/Configuration/Config.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs @@ -1,53 +1,24 @@ using System; using System.Configuration; using System.IO; -using System.Reflection; using System.Web.Configuration; -using System.Web.Hosting; -using Microsoft.CodeAnalysis.CSharp; using Umbraco.Core; +using Umbraco.Core.IO; -namespace Umbraco.ModelsBuilder.Configuration +namespace Umbraco.ModelsBuilder.Embedded.Configuration { /// /// Represents the models builder configuration. /// - public class Config + public class ModelsBuilderConfig : IModelsBuilderConfig { - private static Config _value; + public const string DefaultModelsNamespace = "Umbraco.Web.PublishedModels"; + public const string DefaultModelsDirectory = "~/App_Data/Models"; /// - /// Gets the configuration - internal so that the UmbracoConfig extension - /// can get the value to initialize its own value. Either a value has - /// been provided via the Setup method, or a new instance is created, which - /// will load settings from the config file. + /// Initializes a new instance of the class. /// - internal static Config Value => _value ?? new Config(); - - /// - /// Sets the configuration programmatically. - /// - /// The configuration. - /// - /// Once the configuration has been accessed via the UmbracoConfig extension, - /// it cannot be changed anymore, and using this method will achieve nothing. - /// For tests, see UmbracoConfigExtensions.ResetConfig(). - /// - public static void Setup(Config config) - { - _value = config; - } - - internal const string DefaultStaticMixinGetterPattern = "Get{0}"; - internal const LanguageVersion DefaultLanguageVersion = LanguageVersion.CSharp6; - internal const string DefaultModelsNamespace = "Umbraco.Web.PublishedModels"; - internal const ClrNameSource DefaultClrNameSource = ClrNameSource.Alias; // for legacy reasons - internal const string DefaultModelsDirectory = "~/App_Data/Models"; - - /// - /// Initializes a new instance of the class. - /// - private Config() + public ModelsBuilderConfig() { const string prefix = "Umbraco.ModelsBuilder."; @@ -56,13 +27,8 @@ namespace Umbraco.ModelsBuilder.Configuration Enable = ConfigurationManager.AppSettings[prefix + "Enable"] == "true"; // ensure defaults are initialized for tests - StaticMixinGetterPattern = DefaultStaticMixinGetterPattern; - LanguageVersion = DefaultLanguageVersion; ModelsNamespace = DefaultModelsNamespace; - ClrNameSource = DefaultClrNameSource; - ModelsDirectory = HostingEnvironment.IsHosted - ? HostingEnvironment.MapPath(DefaultModelsDirectory) - : DefaultModelsDirectory.TrimStart("~/"); + ModelsDirectory = IOHelper.MapPath(DefaultModelsDirectory); DebugLevel = 0; // stop here, everything is false @@ -80,12 +46,6 @@ namespace Umbraco.ModelsBuilder.Configuration case nameof(ModelsMode.PureLive): ModelsMode = ModelsMode.PureLive; break; - case nameof(ModelsMode.Dll): - ModelsMode = ModelsMode.Dll; - break; - case nameof(ModelsMode.LiveDll): - ModelsMode = ModelsMode.LiveDll; - break; case nameof(ModelsMode.AppData): ModelsMode = ModelsMode.AppData; break; @@ -94,17 +54,15 @@ namespace Umbraco.ModelsBuilder.Configuration break; default: throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode." - + " Note that modes are case-sensitive."); + + " Note that modes are case-sensitive. Possible values are: " + string.Join(", ", Enum.GetNames(typeof(ModelsMode)))); } } // default: false - EnableApi = ConfigurationManager.AppSettings[prefix + "EnableApi"].InvariantEquals("true"); AcceptUnsafeModelsDirectory = ConfigurationManager.AppSettings[prefix + "AcceptUnsafeModelsDirectory"].InvariantEquals("true"); // default: true EnableFactory = !ConfigurationManager.AppSettings[prefix + "EnableFactory"].InvariantEquals("false"); - StaticMixinGetters = !ConfigurationManager.AppSettings[prefix + "StaticMixinGetters"].InvariantEquals("false"); FlagOutOfDateModels = !ConfigurationManager.AppSettings[prefix + "FlagOutOfDateModels"].InvariantEquals("false"); // default: initialized above with DefaultModelsNamespace const @@ -112,52 +70,11 @@ namespace Umbraco.ModelsBuilder.Configuration if (!string.IsNullOrWhiteSpace(value)) ModelsNamespace = value; - // default: initialized above with DefaultStaticMixinGetterPattern const - value = ConfigurationManager.AppSettings[prefix + "StaticMixinGetterPattern"]; - if (!string.IsNullOrWhiteSpace(value)) - StaticMixinGetterPattern = value; - - // default: initialized above with DefaultLanguageVersion const - value = ConfigurationManager.AppSettings[prefix + "LanguageVersion"]; - if (!string.IsNullOrWhiteSpace(value)) - { - LanguageVersion lv; - if (!Enum.TryParse(value, true, out lv)) - throw new ConfigurationErrorsException($"Invalid language version \"{value}\"."); - LanguageVersion = lv; - } - - // default: initialized above with DefaultClrNameSource const - value = ConfigurationManager.AppSettings[prefix + "ClrNameSource"]; - if (!string.IsNullOrWhiteSpace(value)) - { - switch (value) - { - case nameof(ClrNameSource.Nothing): - ClrNameSource = ClrNameSource.Nothing; - break; - case nameof(ClrNameSource.Alias): - ClrNameSource = ClrNameSource.Alias; - break; - case nameof(ClrNameSource.RawAlias): - ClrNameSource = ClrNameSource.RawAlias; - break; - case nameof(ClrNameSource.Name): - ClrNameSource = ClrNameSource.Name; - break; - default: - throw new ConfigurationErrorsException($"ClrNameSource \"{value}\" is not a valid source." - + " Note that sources are case-sensitive."); - } - } - // default: initialized above with DefaultModelsDirectory const value = ConfigurationManager.AppSettings[prefix + "ModelsDirectory"]; if (!string.IsNullOrWhiteSpace(value)) { - var root = HostingEnvironment.IsHosted - ? HostingEnvironment.MapPath("~/") - : Directory.GetCurrentDirectory(); + var root = IOHelper.MapPath("~/"); if (root == null) throw new ConfigurationErrorsException("Could not determine root directory."); @@ -181,19 +98,14 @@ namespace Umbraco.ModelsBuilder.Configuration } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public Config( + public ModelsBuilderConfig( bool enable = false, ModelsMode modelsMode = ModelsMode.Nothing, - bool enableApi = true, string modelsNamespace = null, bool enableFactory = true, - LanguageVersion languageVersion = DefaultLanguageVersion, - bool staticMixinGetters = true, - string staticMixinGetterPattern = null, bool flagOutOfDateModels = true, - ClrNameSource clrNameSource = DefaultClrNameSource, string modelsDirectory = null, bool acceptUnsafeModelsDirectory = false, int debugLevel = 0) @@ -201,14 +113,9 @@ namespace Umbraco.ModelsBuilder.Configuration Enable = enable; ModelsMode = modelsMode; - EnableApi = enableApi; ModelsNamespace = string.IsNullOrWhiteSpace(modelsNamespace) ? DefaultModelsNamespace : modelsNamespace; EnableFactory = enableFactory; - LanguageVersion = languageVersion; - StaticMixinGetters = staticMixinGetters; - StaticMixinGetterPattern = string.IsNullOrWhiteSpace(staticMixinGetterPattern) ? DefaultStaticMixinGetterPattern : staticMixinGetterPattern; FlagOutOfDateModels = flagOutOfDateModels; - ClrNameSource = clrNameSource; ModelsDirectory = string.IsNullOrWhiteSpace(modelsDirectory) ? DefaultModelsDirectory : modelsDirectory; AcceptUnsafeModelsDirectory = acceptUnsafeModelsDirectory; DebugLevel = debugLevel; @@ -259,27 +166,6 @@ namespace Umbraco.ModelsBuilder.Configuration /// public ModelsMode ModelsMode { get; } - /// - /// Gets a value indicating whether to serve the API. - /// - public bool ApiServer => EnableApi && ApiInstalled && IsDebug; - - /// - /// Gets a value indicating whether to enable the API. - /// - /// - /// Default value is true. - /// The API is used by the Visual Studio extension and the console tool to talk to Umbraco - /// and retrieve the content types. It needs to be enabled so the extension & tool can work. - /// - public bool EnableApi { get; } - - /// - /// Gets a value indicating whether the API is installed. - /// - // fixme - this is now always true as the API is part of Core - public bool ApiInstalled => true; - /// /// Gets a value indicating whether system.web/compilation/@debug is true. /// @@ -287,7 +173,7 @@ namespace Umbraco.ModelsBuilder.Configuration { get { - var section = (CompilationSection) ConfigurationManager.GetSection("system.web/compilation"); + var section = (CompilationSection)ConfigurationManager.GetSection("system.web/compilation"); return section != null && section.Debug; } } @@ -304,24 +190,6 @@ namespace Umbraco.ModelsBuilder.Configuration /// Default value is true because no factory is enabled by default in Umbraco. public bool EnableFactory { get; } - /// - /// Gets the Roslyn parser language version. - /// - /// Default value is CSharp6. - public LanguageVersion LanguageVersion { get; } - - /// - /// Gets a value indicating whether to generate static mixin getters. - /// - /// Default value is false for backward compat reaons. - public bool StaticMixinGetters { get; } - - /// - /// Gets the string pattern for mixin properties static getter name. - /// - /// Default value is "GetXxx". Standard string format. - public string StaticMixinGetterPattern { get; } - /// /// Gets a value indicating whether we should flag out-of-date models. /// @@ -330,11 +198,6 @@ namespace Umbraco.ModelsBuilder.Configuration /// generated through the dashboard, the files is cleared. Default value is false. public bool FlagOutOfDateModels { get; } - /// - /// Gets the CLR name source. - /// - public ClrNameSource ClrNameSource { get; } - /// /// Gets the models directory. /// diff --git a/src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsMode.cs similarity index 56% rename from src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs rename to src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsMode.cs index e04c4dee90..e0286fdab1 100644 --- a/src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsMode.cs @@ -1,4 +1,4 @@ -namespace Umbraco.ModelsBuilder.Configuration +namespace Umbraco.ModelsBuilder.Embedded.Configuration { /// /// Defines the models generation modes. @@ -8,7 +8,7 @@ /// /// Do not generate models. /// - Nothing = 0, // default value + Nothing = 0, // default value /// /// Generate models in memory. @@ -31,22 +31,6 @@ /// /// Generation can be triggered from the dashboard. The app does not restart. /// Models are not compiled and thus are not available to the project. - LiveAppData, - - /// - /// Generates models in AppData and compiles them into a Dll into ~/bin (the app restarts). - /// When: generation is triggered. - /// - /// Generation can be triggered from the dashboard. The app does restart. Models - /// are available to the entire project. - Dll, - - /// - /// Generates models in AppData and compiles them into a Dll into ~/bin (the app restarts). - /// When: a content type change occurs, or generation is triggered. - /// - /// Generation can be triggered from the dashboard. The app does restart. Models - /// are available to the entire project. - LiveDll + LiveAppData } } diff --git a/src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsModeExtensions.cs similarity index 61% rename from src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs rename to src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsModeExtensions.cs index be609c0548..be638729ea 100644 --- a/src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsModeExtensions.cs @@ -1,4 +1,4 @@ -namespace Umbraco.ModelsBuilder.Configuration +namespace Umbraco.ModelsBuilder.Embedded.Configuration { /// /// Provides extensions for the enumeration. @@ -12,7 +12,6 @@ { return modelsMode == ModelsMode.PureLive - || modelsMode == ModelsMode.LiveDll || modelsMode == ModelsMode.LiveAppData; } @@ -22,18 +21,7 @@ public static bool IsLiveNotPure(this ModelsMode modelsMode) { return - modelsMode == ModelsMode.LiveDll - || modelsMode == ModelsMode.LiveAppData; - } - - /// - /// Gets a value indicating whether the mode is [Live]Dll. - /// - public static bool IsAnyDll(this ModelsMode modelsMode) - { - return - modelsMode == ModelsMode.Dll - || modelsMode == ModelsMode.LiveDll; + modelsMode == ModelsMode.LiveAppData; } /// @@ -42,10 +30,8 @@ public static bool SupportsExplicitGeneration(this ModelsMode modelsMode) { return - modelsMode == ModelsMode.Dll - || modelsMode == ModelsMode.LiveDll - || modelsMode == ModelsMode.AppData + modelsMode == ModelsMode.AppData || modelsMode == ModelsMode.LiveAppData; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs b/src/Umbraco.ModelsBuilder.Embedded/HashCombiner.cs similarity index 76% rename from src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs rename to src/Umbraco.ModelsBuilder.Embedded/HashCombiner.cs index e11662eb24..1c1fca6f73 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/HashCombiner.cs @@ -1,17 +1,17 @@ using System; using System.Globalization; -namespace Umbraco.ModelsBuilder.Umbraco +namespace Umbraco.ModelsBuilder.Embedded { // because, of course, it's internal in Umbraco // see also System.Web.Util.HashCodeCombiner - class HashCombiner + internal class HashCombiner { private long _combinedHash = 5381L; public void Add(int i) { - _combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i; + _combinedHash = (_combinedHash << 5) + _combinedHash ^ i; } public void Add(object o) @@ -27,7 +27,7 @@ namespace Umbraco.ModelsBuilder.Umbraco public void Add(string s) { if (s == null) return; - Add((StringComparer.InvariantCulture).GetHashCode(s)); + Add(StringComparer.InvariantCulture.GetHashCode(s)); } public string GetCombinedHashCode() diff --git a/src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs similarity index 79% rename from src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs rename to src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs index c5d8f8cad4..0359c49654 100644 --- a/src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs @@ -1,10 +1,6 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace Umbraco.ModelsBuilder +namespace Umbraco.ModelsBuilder.Embedded { /// /// Indicates that a property implements a given property alias. diff --git a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs new file mode 100644 index 0000000000..333181f27c --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs @@ -0,0 +1,110 @@ +using System; +using System.Threading; +using System.Web.Hosting; +using Umbraco.Core.Logging; +using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web.Cache; + +namespace Umbraco.ModelsBuilder.Embedded +{ + // supports LiveAppData - but not PureLive + public sealed class LiveModelsProvider + { + private static Mutex _mutex; + private static int _req; + private readonly ILogger _logger; + private readonly IModelsBuilderConfig _config; + private readonly ModelsGenerator _modelGenerator; + private readonly ModelsGenerationError _mbErrors; + + // we do not manage pure live here + internal bool IsEnabled => _config.ModelsMode.IsLiveNotPure(); + + public LiveModelsProvider(ILogger logger, IModelsBuilderConfig config, ModelsGenerator modelGenerator, ModelsGenerationError mbErrors) + { + _logger = logger; + _config = config ?? throw new ArgumentNullException(nameof(config)); + _modelGenerator = modelGenerator; + _mbErrors = mbErrors; + } + + internal void Install() + { + // just be sure + if (!IsEnabled) + return; + + // initialize mutex + // ApplicationId will look like "/LM/W3SVC/1/Root/AppName" + // name is system-wide and must be less than 260 chars + var name = HostingEnvironment.ApplicationID + "/UmbracoLiveModelsProvider"; + + _mutex = new Mutex(false, name); //TODO: Replace this with MainDom? Seems we now have 2x implementations of almost the same thing + + // anything changes, and we want to re-generate models. + ContentTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; + DataTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; + + // at the end of a request since we're restarting the pool + // NOTE - this does NOT trigger - see module below + //umbracoApplication.EndRequest += GenerateModelsIfRequested; + } + + // NOTE + // Using HttpContext Items fails because CacheUpdated triggers within + // some asynchronous backend task where we seem to have no HttpContext. + + // So we use a static (non request-bound) var to register that models + // need to be generated. Could be by another request. Anyway. We could + // have collisions but... you know the risk. + + private void RequestModelsGeneration(object sender, EventArgs args) + { + //HttpContext.Current.Items[this] = true; + _logger.Debug("Requested to generate models."); + Interlocked.Exchange(ref _req, 1); + } + + public void GenerateModelsIfRequested(object sender, EventArgs args) + { + //if (HttpContext.Current.Items[this] == null) return; + if (Interlocked.Exchange(ref _req, 0) == 0) return; + + // cannot use a simple lock here because we don't want another AppDomain + // to generate while we do... and there could be 2 AppDomains if the app restarts. + + try + { + _logger.Debug("Generate models..."); + const int timeout = 2 * 60 * 1000; // 2 mins + _mutex.WaitOne(timeout); // wait until it is safe, and acquire + _logger.Info("Generate models now."); + GenerateModels(); + _mbErrors.Clear(); + _logger.Info("Generated."); + } + catch (TimeoutException) + { + _logger.Warn("Timeout, models were NOT generated."); + } + catch (Exception e) + { + _mbErrors.Report("Failed to build Live models.", e); + _logger.Error("Failed to generate models.", e); + } + finally + { + _mutex.ReleaseMutex(); // release + } + } + + private void GenerateModels() + { + // EnableDllModels will recycle the app domain - but this request will end properly + _modelGenerator.GenerateModels(); + } + + + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProviderModule.cs b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProviderModule.cs new file mode 100644 index 0000000000..678ff241b0 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProviderModule.cs @@ -0,0 +1,47 @@ +using System; +using System.Web; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.ModelsBuilder.Embedded; + +// will install only if configuration says it needs to be installed +[assembly: PreApplicationStartMethod(typeof(LiveModelsProviderModule), "Install")] + +namespace Umbraco.ModelsBuilder.Embedded +{ + // have to do this because it's the only way to subscribe to EndRequest, + // module is installed by assembly attribute at the top of this file + public class LiveModelsProviderModule : IHttpModule + { + private static LiveModelsProvider _liveModelsProvider; + + public void Init(HttpApplication app) + { + app.EndRequest += App_EndRequest; + } + + private void App_EndRequest(object sender, EventArgs e) + { + if (((HttpApplication)sender).Request.Url.IsClientSideRequest()) + return; + + // here we're using "Current." since we're in a module, it is possible in a round about way to inject into a module but for now we'll just use Current + if (_liveModelsProvider == null) + _liveModelsProvider = Current.Factory.TryGetInstance(); // will be null in upgrade mode or if embedded MB is disabled + + if (_liveModelsProvider?.IsEnabled ?? false) + _liveModelsProvider.GenerateModelsIfRequested(sender, e); + } + + public void Dispose() + { + // nothing + } + + public static void Install() + { + // always - don't read config in PreApplicationStartMethod + HttpApplication.RegisterModule(typeof(LiveModelsProviderModule)); + } + } +} diff --git a/src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderAssemblyAttribute.cs similarity index 95% rename from src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs rename to src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderAssemblyAttribute.cs index ed956852f8..7570c0b5b2 100644 --- a/src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderAssemblyAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace Umbraco.ModelsBuilder +namespace Umbraco.ModelsBuilder.Embedded { /// /// Indicates that an Assembly is a Models Builder assembly. diff --git a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs new file mode 100644 index 0000000000..b8b1945f32 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs @@ -0,0 +1,19 @@ +using System; +using Umbraco.Core.Composing; +using Umbraco.Core.Dashboards; + +namespace Umbraco.ModelsBuilder.Embedded +{ + [Weight(40)] + public class ModelsBuilderDashboard : IDashboard + { + public string Alias => "settingsModelsBuilder"; + + public string[] Sections => new [] { "settings" }; + + public string View => "views/dashboard/settings/modelsbuildermanagement.html"; + + public IAccessRule[] AccessRules => Array.Empty(); + } + +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs similarity index 68% rename from src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs rename to src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs index 7102190b5e..a692f633a5 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs @@ -1,14 +1,20 @@ using System; using System.IO; using System.Text; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; +using Umbraco.ModelsBuilder.Embedded.Configuration; -namespace Umbraco.ModelsBuilder.Umbraco +namespace Umbraco.ModelsBuilder.Embedded { - internal static class ModelsGenerationError + public sealed class ModelsGenerationError { - public static void Clear() + private readonly IModelsBuilderConfig _config; + + public ModelsGenerationError(IModelsBuilderConfig config) + { + _config = config; + } + + public void Clear() { var errFile = GetErrFile(); if (errFile == null) return; @@ -17,7 +23,7 @@ namespace Umbraco.ModelsBuilder.Umbraco File.Delete(errFile); } - public static void Report(string message, Exception e) + public void Report(string message, Exception e) { var errFile = GetErrFile(); if (errFile == null) return; @@ -33,7 +39,7 @@ namespace Umbraco.ModelsBuilder.Umbraco File.WriteAllText(errFile, sb.ToString()); } - public static string GetLastError() + public string GetLastError() { var errFile = GetErrFile(); if (errFile == null) return null; @@ -48,9 +54,9 @@ namespace Umbraco.ModelsBuilder.Umbraco } } - private static string GetErrFile() + private string GetErrFile() { - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; + var modelsDirectory = _config.ModelsDirectory; if (!Directory.Exists(modelsDirectory)) return null; diff --git a/src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs b/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs similarity index 55% rename from src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs rename to src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs index a047f21edb..5425c31c77 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs @@ -1,55 +1,58 @@ -using System; -using System.IO; -using System.Web.Hosting; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; +using System.IO; +using Umbraco.ModelsBuilder.Embedded.Configuration; using Umbraco.Web.Cache; -namespace Umbraco.ModelsBuilder.Umbraco +namespace Umbraco.ModelsBuilder.Embedded { public sealed class OutOfDateModelsStatus { - internal static void Install() + private readonly IModelsBuilderConfig _config; + + public OutOfDateModelsStatus(IModelsBuilderConfig config) + { + _config = config; + } + + internal void Install() { // just be sure - if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) + if (_config.FlagOutOfDateModels == false) return; ContentTypeCacheRefresher.CacheUpdated += (sender, args) => Write(); DataTypeCacheRefresher.CacheUpdated += (sender, args) => Write(); } - private static string GetFlagPath() + private string GetFlagPath() { - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; + var modelsDirectory = _config.ModelsDirectory; if (!Directory.Exists(modelsDirectory)) Directory.CreateDirectory(modelsDirectory); return Path.Combine(modelsDirectory, "ood.flag"); } - private static void Write() + private void Write() { var path = GetFlagPath(); if (path == null || File.Exists(path)) return; File.WriteAllText(path, "THIS FILE INDICATES THAT MODELS ARE OUT-OF-DATE\n\n"); } - public static void Clear() + public void Clear() { - if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) return; + if (_config.FlagOutOfDateModels == false) return; var path = GetFlagPath(); if (path == null || !File.Exists(path)) return; File.Delete(path); } - public static bool IsEnabled => UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels; + public bool IsEnabled => _config.FlagOutOfDateModels; - public static bool IsOutOfDate + public bool IsOutOfDate { get { - if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) return false; + if (_config.FlagOutOfDateModels == false) return false; var path = GetFlagPath(); return path != null && File.Exists(path); } diff --git a/src/Umbraco.ModelsBuilder.Embedded/Properties/AssemblyInfo.cs b/src/Umbraco.ModelsBuilder.Embedded/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..68c149adde --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/Properties/AssemblyInfo.cs @@ -0,0 +1,13 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Umbraco.ModelsBuilder")] +[assembly: AssemblyDescription("Umbraco ModelsBuilder")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyProduct("Umbraco CMS")] + +[assembly: ComVisible(false)] +[assembly: Guid("52ac0ba8-a60e-4e36-897b-e8b97a54ed1c")] + +[assembly: InternalsVisibleTo("Umbraco.Tests")] diff --git a/src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs similarity index 80% rename from src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs rename to src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs index f3320b5dfb..29429ba74f 100644 --- a/src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs @@ -2,9 +2,12 @@ using System.Linq.Expressions; using System.Reflection; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web; +using Umbraco.ModelsBuilder; +using Umbraco.ModelsBuilder.Embedded; -namespace Umbraco.ModelsBuilder +// same namespace as original Umbraco.Web PublishedElementExtensions +// ReSharper disable once CheckNamespace +namespace Umbraco.Web { /// /// Provides extension methods to models. @@ -14,13 +17,14 @@ namespace Umbraco.ModelsBuilder /// /// Gets the value of a property. /// - public static TValue Value(this TModel model, Expression> property, string culture = ".", string segment = ".") + public static TValue Value(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) where TModel : IPublishedElement { var alias = GetAlias(model, property); - return model.Value(alias, culture, segment); + return model.Value(alias, culture, segment, fallback, defaultValue); } + // fixme that one should be public so ppl can use it private static string GetAlias(TModel model, Expression> property) { if (property.NodeType != ExpressionType.Lambda) diff --git a/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs b/src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs similarity index 77% rename from src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs rename to src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs index c70e8a3b65..8a6ed83ce9 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs @@ -1,11 +1,17 @@ using System; using System.Linq; using System.Linq.Expressions; -using Umbraco.Web.Composing; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Composing; -namespace Umbraco.ModelsBuilder.Umbraco +namespace Umbraco.ModelsBuilder.Embedded { + /// + /// This is called from within the generated model classes + /// + /// + /// DO NOT REMOVE - although there are not code references this is used directly by the generated models. + /// public static class PublishedModelUtility { // looks safer but probably useless... ppl should not call these methods directly @@ -24,7 +30,7 @@ namespace Umbraco.ModelsBuilder.Umbraco // // etc... //} - public static PublishedContentType GetModelContentType(PublishedItemType itemType, string alias) + public static IPublishedContentType GetModelContentType(PublishedItemType itemType, string alias) { var facade = Current.UmbracoContext.PublishedSnapshot; // fixme inject! switch (itemType) @@ -40,8 +46,8 @@ namespace Umbraco.ModelsBuilder.Umbraco } } - public static PublishedPropertyType GetModelPropertyType(PublishedContentType contentType, Expression> selector) - //where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel + public static IPublishedPropertyType GetModelPropertyType(IPublishedContentType contentType, Expression> selector) + //where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel { // fixme therefore, missing a check on TModel here @@ -54,7 +60,7 @@ namespace Umbraco.ModelsBuilder.Umbraco // see note above : accepted risk... var attr = expr.Member - .GetCustomAttributes(typeof (ImplementPropertyTypeAttribute), false) + .GetCustomAttributes(typeof(ImplementPropertyTypeAttribute), false) .OfType() .SingleOrDefault(); diff --git a/src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs similarity index 72% rename from src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs rename to src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs index 9558c0140e..8e8a19c729 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs @@ -3,59 +3,60 @@ using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; using System.Text; using System.Text.RegularExpressions; using System.Threading; +using System.Web; using System.Web.Compilation; using System.Web.Hosting; using System.Web.WebPages.Razor; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; -using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web.Cache; -using Umbraco.ModelsBuilder.Building; -using Umbraco.ModelsBuilder.Configuration; +using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.ModelsBuilder.Embedded.Configuration; using File = System.IO.File; -namespace Umbraco.ModelsBuilder.Umbraco +namespace Umbraco.ModelsBuilder.Embedded { - internal class PureLiveModelFactory : IPublishedModelFactory, IRegisteredObject + internal class PureLiveModelFactory : ILivePublishedModelFactory, IRegisteredObject { private Assembly _modelsAssembly; private Infos _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary() }; private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); private volatile bool _hasModels; // volatile 'cos reading outside lock private bool _pendingRebuild; - private readonly ProfilingLogger _logger; + private readonly IProfilingLogger _logger; private readonly FileSystemWatcher _watcher; private int _ver, _skipver; private readonly int _debugLevel; private BuildManager _theBuildManager; - private readonly Lazy _umbracoServices; + private readonly Lazy _umbracoServices; // fixme: this is because of circular refs :( private UmbracoServices UmbracoServices => _umbracoServices.Value; private static readonly Regex AssemblyVersionRegex = new Regex("AssemblyVersion\\(\"[0-9]+.[0-9]+.[0-9]+.[0-9]+\"\\)", RegexOptions.Compiled); private const string ProjVirt = "~/App_Data/Models/all.generated.cs"; private static readonly string[] OurFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err" }; - public PureLiveModelFactory(Lazy umbracoServices, ProfilingLogger logger) + private readonly IModelsBuilderConfig _config; + private readonly ModelsGenerationError _errors; + + public PureLiveModelFactory(Lazy umbracoServices, IProfilingLogger logger, IModelsBuilderConfig config) { _umbracoServices = umbracoServices; _logger = logger; + _config = config; + _errors = new ModelsGenerationError(config); _ver = 1; // zero is for when we had no version _skipver = -1; // nothing to skip - ContentTypeCacheRefresher.CacheUpdated += (sender, args) => ResetModels(); - DataTypeCacheRefresher.CacheUpdated += (sender, args) => ResetModels(); + RazorBuildProvider.CodeGenerationStarted += RazorBuildProvider_CodeGenerationStarted; if (!HostingEnvironment.IsHosted) return; - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; + var modelsDirectory = _config.ModelsDirectory; if (!Directory.Exists(modelsDirectory)) Directory.CreateDirectory(modelsDirectory); @@ -68,9 +69,23 @@ namespace Umbraco.ModelsBuilder.Umbraco _watcher.EnableRaisingEvents = true; // get it here, this need to be fast - _debugLevel = UmbracoConfig.For.ModelsBuilder().DebugLevel; + _debugLevel = _config.DebugLevel; } + #region ILivePublishedModelFactory + + /// + public object SyncRoot { get; } = new object(); + + /// + public void Refresh() + { + ResetModels(); + EnsureModels(); + } + + #endregion + #region IPublishedModelFactory public IPublishedElement CreateModel(IPublishedElement element) @@ -84,7 +99,7 @@ namespace Umbraco.ModelsBuilder.Umbraco var contentTypeAlias = element.ContentType.Alias; // lookup model constructor (else null) - infos.TryGetValue(contentTypeAlias, out ModelInfo info); + infos.TryGetValue(contentTypeAlias, out var info); // create model return info == null ? element : info.Ctor(element); @@ -115,7 +130,7 @@ namespace Umbraco.ModelsBuilder.Umbraco if (ctor != null) return ctor(); var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); - ctor = modelInfo.ListCtor = ReflectionUtilities.EmitCtor>(declaring: listType); + ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor>(declaring: listType); return ctor(); } @@ -163,7 +178,7 @@ namespace Umbraco.ModelsBuilder.Umbraco if (_modelsAssembly == null) return; if (_debugLevel > 0) - _logger.Logger.Debug("RazorBuildProvider.CodeGenerationStarted"); + _logger.Debug("RazorBuildProvider.CodeGenerationStarted"); if (!(sender is RazorBuildProvider provider)) return; // add the assembly, and add a dependency to a text file that will change on each @@ -182,7 +197,7 @@ namespace Umbraco.ModelsBuilder.Umbraco // tells the factory that it should build a new generation of models private void ResetModels() { - _logger.Logger.Debug("Resetting models."); + _logger.Debug("Resetting models."); try { @@ -190,6 +205,19 @@ namespace Umbraco.ModelsBuilder.Umbraco _hasModels = false; _pendingRebuild = true; + + var modelsDirectory = _config.ModelsDirectory; + if (!Directory.Exists(modelsDirectory)) + Directory.CreateDirectory(modelsDirectory); + + // clear stuff + var modelsHashFile = Path.Combine(modelsDirectory, "models.hash"); + //var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs"); + //var projFile = Path.Combine(modelsDirectory, "all.generated.cs"); + var dllPathFile = Path.Combine(modelsDirectory, "all.dll.path"); + + if (File.Exists(dllPathFile)) File.Delete(dllPathFile); + if (File.Exists(modelsHashFile)) File.Delete(modelsHashFile); } finally { @@ -204,10 +232,10 @@ namespace Umbraco.ModelsBuilder.Umbraco get { if (_theBuildManager != null) return _theBuildManager; - var prop = typeof (BuildManager).GetProperty("TheBuildManager", BindingFlags.NonPublic | BindingFlags.Static); + var prop = typeof(BuildManager).GetProperty("TheBuildManager", BindingFlags.NonPublic | BindingFlags.Static); if (prop == null) throw new InvalidOperationException("Could not get BuildManager.TheBuildManager property."); - _theBuildManager = (BuildManager) prop.GetValue(null); + _theBuildManager = (BuildManager)prop.GetValue(null); return _theBuildManager; } } @@ -216,7 +244,7 @@ namespace Umbraco.ModelsBuilder.Umbraco internal Infos EnsureModels() { if (_debugLevel > 0) - _logger.Logger.Debug("Ensuring models."); + _logger.Debug("Ensuring models."); // don't use an upgradeable lock here because only 1 thread at a time could enter it try @@ -264,15 +292,15 @@ namespace Umbraco.ModelsBuilder.Umbraco var types = assembly.ExportedTypes.Where(x => x.Inherits() || x.Inherits()); _infos = RegisterModels(types); - ModelsGenerationError.Clear(); + _errors.Clear(); } catch (Exception e) { try { - _logger.Logger.Error("Failed to build models.", e); - _logger.Logger.Warn("Running without models."); // be explicit - ModelsGenerationError.Report("Failed to build PureLive models.", e); + _logger.Error("Failed to build models.", e); + _logger.Warn("Running without models."); // be explicit + _errors.Report("Failed to build PureLive models.", e); } finally { @@ -300,19 +328,12 @@ namespace Umbraco.ModelsBuilder.Umbraco private Assembly GetModelsAssembly(bool forceRebuild) { - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; + var modelsDirectory = _config.ModelsDirectory; if (!Directory.Exists(modelsDirectory)) Directory.CreateDirectory(modelsDirectory); - // must filter out *.generated.cs because we haven't deleted them yet! - var ourFiles = Directory.Exists(modelsDirectory) - ? Directory.GetFiles(modelsDirectory, "*.cs") - .Where(x => !x.EndsWith(".generated.cs")) - .ToDictionary(x => x, File.ReadAllText) - : new Dictionary(); - var typeModels = UmbracoServices.GetAllTypes(); - var currentHash = HashHelper.Hash(ourFiles, typeModels); + var currentHash = TypeModelHasher.Hash(typeModels); var modelsHashFile = Path.Combine(modelsDirectory, "models.hash"); var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs"); var projFile = Path.Combine(modelsDirectory, "all.generated.cs"); @@ -323,31 +344,48 @@ namespace Umbraco.ModelsBuilder.Umbraco if (!forceRebuild) { - _logger.Logger.Debug("Looking for cached models."); + _logger.Debug("Looking for cached models."); if (File.Exists(modelsHashFile) && File.Exists(projFile)) { var cachedHash = File.ReadAllText(modelsHashFile); if (currentHash != cachedHash) { - _logger.Logger.Debug("Found obsolete cached models."); + _logger.Debug("Found obsolete cached models."); forceRebuild = true; } + + // else cachedHash matches currentHash, we can try to load an existing dll } else { - _logger.Logger.Debug("Could not find cached models."); + _logger.Debug("Could not find cached models."); forceRebuild = true; } } Assembly assembly; - if (forceRebuild == false) + if (!forceRebuild) { // try to load the dll directly (avoid rebuilding) + // + // ensure that the .dll file does not have a corresponding .dll.delete file + // as that would mean the the .dll file is going to be deleted and should not + // be re-used - that should not happen in theory, but better be safe + // + // ensure that the .dll file is in the current codegen directory - when IIS + // or Express does a full restart, it can switch to an entirely new codegen + // directory, and then we end up referencing a dll which is *not* in that + // directory, and BuildManager fails to instantiate views ("the view found + // at ... was not created"). + // if (File.Exists(dllPathFile)) { var dllPath = File.ReadAllText(dllPathFile); - if (File.Exists(dllPath)) + var codegen = HttpRuntime.CodegenDir; + + _logger.Debug($"Cached models dll at {dllPath}."); + + if (File.Exists(dllPath) && !File.Exists(dllPath + ".delete") && dllPath.StartsWith(codegen)) { assembly = Assembly.LoadFile(dllPath); var attr = assembly.GetCustomAttribute(); @@ -359,13 +397,23 @@ namespace Umbraco.ModelsBuilder.Umbraco // with the "same but different" version of the assembly in memory _skipver = assembly.GetName().Version.Revision; - _logger.Logger.Debug("Loading cached models (dll)."); + _logger.Debug("Loading cached models (dll)."); return assembly; } + + _logger.Debug("Cached models dll cannot be loaded (invalid assembly)."); } + else if (!File.Exists(dllPath)) + _logger.Debug("Cached models dll does not exist."); + else if (File.Exists(dllPath + ".delete")) + _logger.Debug("Cached models dll is marked for deletion."); + else if (!dllPath.StartsWith(codegen)) + _logger.Debug("Cached models dll is in a different codegen directory."); + else + _logger.Debug("Cached models dll cannot be loaded (why?)."); } - // mmust reset the version in the file else it would keep growing + // must reset the version in the file else it would keep growing // loading cached modules only happens when the app restarts var text = File.ReadAllText(projFile); var match = AssemblyVersionRegex.Match(text); @@ -381,47 +429,80 @@ namespace Umbraco.ModelsBuilder.Umbraco //File.WriteAllText(Path.Combine(modelsDirectory, "models.dep"), "VER:" + _ver); _ver++; - assembly = BuildManager.GetCompiledAssembly(ProjVirt); - File.WriteAllText(dllPathFile, assembly.Location); + try + { + assembly = BuildManager.GetCompiledAssembly(ProjVirt); + File.WriteAllText(dllPathFile, assembly.Location); + } + catch + { + ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile); + throw; + } - _logger.Logger.Debug("Loading cached models (source)."); + _logger.Debug("Loading cached models (source)."); return assembly; } // need to rebuild - _logger.Logger.Debug("Rebuilding models."); + _logger.Debug("Rebuilding models."); // generate code, save - var code = GenerateModelsCode(ourFiles, typeModels); + var code = GenerateModelsCode(typeModels); // add extra attributes, // PureLiveAssembly helps identifying Assemblies that contain PureLive models // AssemblyVersion is so that we have a different version for each rebuild var ver = _ver == _skipver ? ++_ver : _ver; _ver++; - code = code.Replace("//ASSATTR", $@"[assembly: PureLiveAssembly] -[assembly:ModelsBuilderAssembly(PureLive = true, SourceHash = ""{currentHash}"")] + code = code.Replace("//ASSATTR", $@"[assembly:ModelsBuilderAssembly(PureLive = true, SourceHash = ""{currentHash}"")] [assembly:System.Reflection.AssemblyVersion(""0.0.0.{ver}"")]"); File.WriteAllText(modelsSrcFile, code); // generate proj, save - ourFiles["models.generated.cs"] = code; - var proj = GenerateModelsProj(ourFiles); + var projFiles = new Dictionary + { + { "models.generated.cs", code } + }; + var proj = GenerateModelsProj(projFiles); File.WriteAllText(projFile, proj); // compile and register - assembly = BuildManager.GetCompiledAssembly(ProjVirt); - File.WriteAllText(dllPathFile, assembly.Location); + try + { + assembly = BuildManager.GetCompiledAssembly(ProjVirt); + File.WriteAllText(dllPathFile, assembly.Location); + File.WriteAllText(modelsHashFile, currentHash); + } + catch + { + ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile); + throw; + } - // assuming we can write and it's not going to cause exceptions... - File.WriteAllText(modelsHashFile, currentHash); - - _logger.Logger.Debug("Done rebuilding."); + _logger.Debug("Done rebuilding."); return assembly; } + private void ClearOnFailingToCompile(string dllPathFile, string modelsHashFile, string projFile) + { + _logger.Debug("Failed to compile."); + + // the dll file reference still points to the previous dll, which is obsolete + // now and will be deleted by ASP.NET eventually, so better clear that reference. + // also touch the proj file to force views to recompile - don't delete as it's + // useful to have the source around for debugging. + try + { + if (File.Exists(dllPathFile)) File.Delete(dllPathFile); + if (File.Exists(modelsHashFile)) File.Delete(modelsHashFile); + if (File.Exists(projFile)) File.SetLastWriteTime(projFile, DateTime.Now); + } + catch { /* enough */ } + } + private static Infos RegisterModels(IEnumerable types) { - var ctorArgTypes = new[] { typeof (IPublishedElement) }; + var ctorArgTypes = new[] { typeof(IPublishedElement) }; var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); var map = new Dictionary(); @@ -433,7 +514,7 @@ namespace Umbraco.ModelsBuilder.Umbraco foreach (var ctor in type.GetConstructors()) { var parms = ctor.GetParameters(); - if (parms.Length == 1 && typeof (IPublishedElement).IsAssignableFrom(parms[0].ParameterType)) + if (parms.Length == 1 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType)) { if (constructor != null) throw new InvalidOperationException($"Type {type.FullName} has more than one public constructor with one argument of type, or implementing, IPropertySet."); @@ -448,16 +529,17 @@ namespace Umbraco.ModelsBuilder.Umbraco var attribute = type.GetCustomAttribute(false); var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias; - if (modelInfos.TryGetValue(typeName, out ModelInfo modelInfo)) + if (modelInfos.TryGetValue(typeName, out var modelInfo)) throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\"."); // fixme use Core's ReflectionUtilities.EmitCtor !! - var meth = new DynamicMethod(string.Empty, typeof (IPublishedElement), ctorArgTypes, type.Module, true); + // Yes .. DynamicMethod is uber slow + var meth = new DynamicMethod(string.Empty, typeof(IPublishedElement), ctorArgTypes, type.Module, true); var gen = meth.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Newobj, constructor); gen.Emit(OpCodes.Ret); - var func = (Func) meth.CreateDelegate(typeof (Func)); + var func = (Func)meth.CreateDelegate(typeof(Func)); modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, Ctor = func, ModelType = type }; map[typeName] = type; @@ -466,26 +548,16 @@ namespace Umbraco.ModelsBuilder.Umbraco return new Infos { ModelInfos = modelInfos.Count > 0 ? modelInfos : null, ModelTypeMap = map }; } - private static string GenerateModelsCode(IDictionary ourFiles, IList typeModels) + private string GenerateModelsCode(IList typeModels) { - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; + var modelsDirectory = _config.ModelsDirectory; if (!Directory.Exists(modelsDirectory)) Directory.CreateDirectory(modelsDirectory); foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) File.Delete(file); - var map = typeModels.ToDictionary(x => x.Alias, x => x.ClrName); - foreach (var typeModel in typeModels) - { - foreach (var propertyModel in typeModel.Properties) - { - propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map); - } - } - - var parseResult = new CodeParser().ParseWithReferencedAssemblies(ourFiles); - var builder = new TextBuilder(typeModels, parseResult, UmbracoConfig.For.ModelsBuilder().ModelsNamespace); + var builder = new TextBuilder(_config, typeModels); var codeBuilder = new StringBuilder(); builder.Generate(codeBuilder, builder.GetModelsToGenerate()); @@ -577,7 +649,7 @@ namespace Umbraco.ModelsBuilder.Umbraco //if (_building && OurFiles.Contains(changed)) //{ - // //_logger.Logger.Info("Ignoring files self-changes."); + // //_logger.Info("Ignoring files self-changes."); // return; //} @@ -585,9 +657,10 @@ namespace Umbraco.ModelsBuilder.Umbraco if (OurFiles.Contains(changed)) return; - _logger.Logger.Info("Detected files changes."); + _logger.Info("Detected files changes."); - ResetModels(); + lock (SyncRoot) // don't reset while being locked + ResetModels(); } public void Stop(bool immediate) diff --git a/src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs b/src/Umbraco.ModelsBuilder.Embedded/ReferencedAssemblies.cs similarity index 54% rename from src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs rename to src/Umbraco.ModelsBuilder.Embedded/ReferencedAssemblies.cs index 42e8b3b9c9..8886afa1c8 100644 --- a/src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/ReferencedAssemblies.cs @@ -4,25 +4,19 @@ using System.Linq; using System.Reflection; using System.Web.Compilation; using System.Web.Hosting; -using Microsoft.CodeAnalysis; using Umbraco.Core; -namespace Umbraco.ModelsBuilder +namespace Umbraco.ModelsBuilder.Embedded { internal static class ReferencedAssemblies { private static readonly Lazy> LazyLocations; - private static readonly Lazy> LazyReferences; static ReferencedAssemblies() { LazyLocations = new Lazy>(() => HostingEnvironment.IsHosted ? GetAllReferencedAssembliesLocationFromBuildManager() : GetAllReferencedAssembliesFromDomain()); - - LazyReferences = new Lazy>(() => Locations - .Select(x => MetadataReference.CreateFromFile(x)) - .ToArray()); } /// @@ -31,19 +25,68 @@ namespace Umbraco.ModelsBuilder /// public static IEnumerable Locations => LazyLocations.Value; - /// - /// Gets the metadata reference of all the referenced assemblies. - /// - public static IEnumerable References => LazyReferences.Value; + public static Assembly GetNetStandardAssembly(List assemblies) + { + if (assemblies == null) + assemblies = BuildManager.GetReferencedAssemblies().Cast().ToList(); - // hosted, get referenced assemblies from the BuildManader and filter + // for some reason, netstandard is also missing from BuildManager.ReferencedAssemblies and yet, is part of + // the references that CSharpCompiler (above) receives - where is it coming from? cannot figure it out + try + { + // so, resorting to an ugly trick + // we should have System.Reflection.Metadata around, and it should reference netstandard + var someAssembly = assemblies.First(x => x.FullName.StartsWith("System.Reflection.Metadata,")); + var netStandardAssemblyName = someAssembly.GetReferencedAssemblies().First(x => x.FullName.StartsWith("netstandard,")); + var netStandard = Assembly.Load(netStandardAssemblyName.FullName); + return netStandard; + } + catch { /* never mind */ } + + return null; + } + + public static Assembly GetNetStandardAssembly() + { + // in PreApplicationStartMethod we cannot get BuildManager.Referenced assemblies, do it differently + try + { + var someAssembly = Assembly.Load("System.Reflection.Metadata"); + var netStandardAssemblyName = someAssembly.GetReferencedAssemblies().First(x => x.FullName.StartsWith("netstandard,")); + var netStandard = Assembly.Load(netStandardAssemblyName.FullName); + return netStandard; + } + catch { /* never mind */ } + + return null; + } + + // hosted, get referenced assemblies from the BuildManager and filter private static IEnumerable GetAllReferencedAssembliesLocationFromBuildManager() { - return BuildManager.GetReferencedAssemblies() - .Cast() + var assemblies = BuildManager.GetReferencedAssemblies().Cast().ToList(); + + assemblies.Add(typeof(ReferencedAssemblies).Assembly); // always include ourselves + + // see https://github.com/aspnet/RoslynCodeDomProvider/blob/master/src/Microsoft.CodeDom.Providers.DotNetCompilerPlatform/CSharpCompiler.cs: + // mentions "Bug 913691: Explicitly add System.Runtime as a reference." + // and explicitly adds System.Runtime to references before invoking csc.exe + // so, doing the same here + try + { + var systemRuntime = Assembly.Load("System.Runtime, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + assemblies.Add(systemRuntime); + } + catch { /* never mind */ } + + // for some reason, netstandard is also missing from BuildManager.ReferencedAssemblies and yet, is part of + // the references that CSharpCompiler (above) receives - where is it coming from? cannot figure it out + var netStandard = GetNetStandardAssembly(assemblies); + if (netStandard != null) assemblies.Add(netStandard); + + return assemblies .Where(x => !x.IsDynamic && !x.Location.IsNullOrWhiteSpace()) .Select(x => x.Location) - .And(typeof(ReferencedAssemblies).Assembly.Location) // always include ourselves .Distinct() .ToList(); } @@ -99,26 +142,6 @@ namespace Umbraco.ModelsBuilder // ---- - private static IEnumerable GetDeepReferencedAssemblies(Assembly assembly) - { - var visiting = new Stack(); - var visited = new HashSet(); - - visiting.Push(assembly); - visited.Add(assembly); - while (visiting.Count > 0) - { - var visAsm = visiting.Pop(); - foreach (var refAsm in visAsm.GetReferencedAssemblies() - .Select(TryLoad) - .Where(x => x != null && visited.Contains(x) == false)) - { - yield return refAsm; - visiting.Push(refAsm); - visited.Add(refAsm); - } - } - } private static Assembly TryLoad(AssemblyName name) { @@ -132,6 +155,5 @@ namespace Umbraco.ModelsBuilder return null; } } - } } diff --git a/src/Umbraco.ModelsBuilder/TypeExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/TypeExtensions.cs similarity index 96% rename from src/Umbraco.ModelsBuilder/TypeExtensions.cs rename to src/Umbraco.ModelsBuilder.Embedded/TypeExtensions.cs index d3b3ff6b4e..1f270a80a6 100644 --- a/src/Umbraco.ModelsBuilder/TypeExtensions.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/TypeExtensions.cs @@ -1,6 +1,6 @@ using System; -namespace Umbraco.ModelsBuilder +namespace Umbraco.ModelsBuilder.Embedded { internal static class TypeExtensions { diff --git a/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj b/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj similarity index 57% rename from src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj rename to src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj index 60ef944a8c..75121a635d 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj +++ b/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj @@ -4,13 +4,15 @@ Debug AnyCPU - {7020A059-C0D1-43A0-8EFD-23591A0C9AF6} + {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C} Library Properties - Umbraco.ModelsBuilder - Umbraco.ModelsBuilder + Umbraco.ModelsBuilder.Embedded + Umbraco.ModelsBuilder.Embedded v4.7.2 512 + true + 7.3 true @@ -28,13 +30,14 @@ TRACE prompt 4 - bin\Release\Umbraco.ModelsBuilder.xml + bin\Release\Umbraco.ModelsBuilder.Embedded.xml + + - @@ -46,62 +49,48 @@ Properties\SolutionInfo.cs - - - - - - - - + + + + - - - - - - - + + + + - - - - - - + - - - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - 2.8.0 + 2.10.0 1.0.0-beta2-19324-01 @@ -111,7 +100,7 @@ - {31785bc3-256c-4613-b2f5-a1b0bdded8c1} + {31785BC3-256C-4613-B2F5-A1B0BDDED8C1} Umbraco.Core @@ -119,5 +108,11 @@ Umbraco.Web + + + 5.2.7 + + + \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs b/src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs similarity index 68% rename from src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs rename to src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs index f0347d9194..5ede5f45e9 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs @@ -2,17 +2,16 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; -using Umbraco.Core.Configuration; +using Umbraco.Core.Exceptions; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Core.Strings; -using Umbraco.ModelsBuilder.Building; -using Umbraco.ModelsBuilder.Configuration; +using Umbraco.ModelsBuilder.Embedded.Building; -namespace Umbraco.ModelsBuilder.Umbraco +namespace Umbraco.ModelsBuilder.Embedded { - public class UmbracoServices + public sealed class UmbracoServices { private readonly IContentTypeService _contentTypeService; private readonly IMediaTypeService _mediaTypeService; @@ -33,6 +32,10 @@ namespace Umbraco.ModelsBuilder.Umbraco { var types = new List(); + // TODO: this will require 3 rather large SQL queries on startup in PureLive. I know that these will be cached after lookup but it will slow + // down startup time ... BUT these queries are also used in NuCache on startup so we can't really avoid them. Maybe one day we can + // load all of these in in one query and still have them cached per service, and/or somehow improve the perf of these since they are used on startup + // in more than one place. types.AddRange(GetTypes(PublishedItemType.Content, _contentTypeService.GetAll().Cast().ToArray())); types.AddRange(GetTypes(PublishedItemType.Media, _mediaTypeService.GetAll().Cast().ToArray())); types.AddRange(GetTypes(PublishedItemType.Member, _memberTypeService.GetAll().Cast().ToArray())); @@ -60,38 +63,8 @@ namespace Umbraco.ModelsBuilder.Umbraco public static string GetClrName(string name, string alias) { - // ideally we should just be able to re-use Umbraco's alias, - // just upper-casing the first letter, however in v7 for backward - // compatibility reasons aliases derive from names via ToSafeAlias which is - // PreFilter = ApplyUrlReplaceCharacters, - // IsTerm = (c, leading) => leading - // ? char.IsLetter(c) // only letters - // : (char.IsLetterOrDigit(c) || c == '_'), // letter, digit or underscore - // StringType = CleanStringType.Ascii | CleanStringType.UmbracoCase, - // BreakTermsOnUpper = false - // - // but that is not ideal with acronyms and casing - // however we CANNOT change Umbraco - // so, adding a way to "do it right" deriving from name, here - - switch (UmbracoConfig.For.ModelsBuilder().ClrNameSource) - { - case ClrNameSource.RawAlias: - // use Umbraco's alias - return alias; - - case ClrNameSource.Alias: - // ModelsBuilder's legacy - but not ideal - return alias.ToCleanString(CleanStringType.ConvertCase | CleanStringType.PascalCase); - - case ClrNameSource.Name: - // derive from name - var source = name.TrimStart('_'); // because CleanStringType.ConvertCase accepts them - return source.ToCleanString(CleanStringType.ConvertCase | CleanStringType.PascalCase | CleanStringType.Ascii); - - default: - throw new Exception("Invalid ClrNameSource."); - } + // ModelsBuilder's legacy - but not ideal + return alias.ToCleanString(CleanStringType.ConvertCase | CleanStringType.PascalCase); } private IList GetTypes(PublishedItemType itemType, IContentTypeComposition[] contentTypes) @@ -116,36 +89,26 @@ namespace Umbraco.ModelsBuilder.Umbraco // of course this should never happen, but when it happens, better detect it // else we end up with weird nullrefs everywhere if (uniqueTypes.Contains(typeModel.ClrName)) - throw new Exception($"Panic: duplicate type ClrName \"{typeModel.ClrName}\"."); + throw new PanicException($"Panic: duplicate type ClrName \"{typeModel.ClrName}\"."); uniqueTypes.Add(typeModel.ClrName); - // fixme - we need a better way at figuring out what's an element type! - // and then we should not do the alias filtering below - bool IsElement(PublishedContentType x) - { - return x.Alias.InvariantEndsWith("Element"); - } - var publishedContentType = _publishedContentTypeFactory.CreateContentType(contentType); switch (itemType) { case PublishedItemType.Content: - if (IsElement(publishedContentType)) - { - typeModel.ItemType = TypeModel.ItemTypes.Element; - if (typeModel.ClrName.InvariantEndsWith("Element")) - typeModel.ClrName = typeModel.ClrName.Substring(0, typeModel.ClrName.Length - "Element".Length); - } - else - { - typeModel.ItemType = TypeModel.ItemTypes.Content; - } + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Content; break; case PublishedItemType.Media: - typeModel.ItemType = TypeModel.ItemTypes.Media; + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Media; break; case PublishedItemType.Member: - typeModel.ItemType = TypeModel.ItemTypes.Member; + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Member; break; default: throw new InvalidOperationException(string.Format("Unsupported PublishedItemType \"{0}\".", itemType)); @@ -166,7 +129,7 @@ namespace Umbraco.ModelsBuilder.Umbraco var publishedPropertyType = publishedContentType.GetPropertyType(propertyType.Alias); if (publishedPropertyType == null) - throw new Exception($"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}."); + throw new PanicException($"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}."); propertyModel.ModelClrType = publishedPropertyType.ModelClrType; @@ -188,7 +151,7 @@ namespace Umbraco.ModelsBuilder.Umbraco foreach (var contentType in contentTypes) { var typeModel = typeModels.SingleOrDefault(x => x.Id == contentType.Id); - if (typeModel == null) throw new Exception("Panic: no type model matching content type."); + if (typeModel == null) throw new PanicException("Panic: no type model matching content type."); IEnumerable compositionTypes; var contentTypeAsMedia = contentType as IMediaType; @@ -197,12 +160,12 @@ namespace Umbraco.ModelsBuilder.Umbraco if (contentTypeAsMedia != null) compositionTypes = contentTypeAsMedia.ContentTypeComposition; else if (contentTypeAsContent != null) compositionTypes = contentTypeAsContent.ContentTypeComposition; else if (contentTypeAsMember != null) compositionTypes = contentTypeAsMember.ContentTypeComposition; - else throw new Exception(string.Format("Panic: unsupported type \"{0}\".", contentType.GetType().FullName)); + else throw new PanicException(string.Format("Panic: unsupported type \"{0}\".", contentType.GetType().FullName)); foreach (var compositionType in compositionTypes) { var compositionModel = typeModels.SingleOrDefault(x => x.Id == compositionType.Id); - if (compositionModel == null) throw new Exception("Panic: composition type does not exist."); + if (compositionModel == null) throw new PanicException("Panic: composition type does not exist."); if (compositionType.Id == contentType.ParentId) continue; @@ -223,11 +186,9 @@ namespace Umbraco.ModelsBuilder.Umbraco { var groups = typeModels.GroupBy(x => x.Alias.ToLowerInvariant()); foreach (var group in groups.Where(x => x.Count() > 1)) - { throw new NotSupportedException($"Alias \"{group.Key}\" is used by types" + $" {string.Join(", ", group.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Aliases have to be unique." + " One of the aliases must be modified in order to use the ModelsBuilder."); - } return typeModels; } diff --git a/src/Umbraco.ModelsBuilder/Api/ApiBasicAuthFilter.cs b/src/Umbraco.ModelsBuilder/Api/ApiBasicAuthFilter.cs deleted file mode 100644 index cc862ff207..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ApiBasicAuthFilter.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Web.Http.Controllers; -using System.Web.Security; -using Umbraco.Core; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models.Membership; - -namespace Umbraco.ModelsBuilder.Api -{ - - //TODO: This needs to be changed: - // * Authentication cannot happen in a filter, only Authorization - // * The filter must be an AuthorizationFilter, not an ActionFilter - // * Authorization must be done using the Umbraco logic - it is very specific for claim checking for ASP.Net Identity - // * Theoretically this shouldn't be required whatsoever because when we authenticate a request that has Basic Auth (i.e. for - // VS to work, it will add the correct Claims to the Identity and it will automatically be authorized. - // - // we *do* have POC supporting ASP.NET identity, however they require some config on the server - // we'll keep using this quick-and-dirty method for the time being - - public class ApiBasicAuthFilter : System.Web.Http.Filters.ActionFilterAttribute // use the http one, not mvc, with api controllers! - { - private static readonly char[] Separator = ":".ToCharArray(); - private readonly string _section; - - public ApiBasicAuthFilter(string section) - { - _section = section; - } - - public override void OnActionExecuting(HttpActionContext actionContext) - { - try - { - var user = Authenticate(actionContext.Request); - if (user == null || !user.AllowedSections.Contains(_section)) - { - actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized); - } - //else - //{ - // // note - would that be a proper way to pass data to the controller? - // // see http://stevescodingblog.co.uk/basic-authentication-with-asp-net-webapi/ - // actionContext.ControllerContext.RouteData.Values["umbraco-user"] = user; - //} - - base.OnActionExecuting(actionContext); - } - catch - { - actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized); - } - } - - private static IUser Authenticate(HttpRequestMessage request) - { - var ah = request.Headers.Authorization; - if (ah == null || ah.Scheme != "Basic") - return null; - - var token = ah.Parameter; - var credentials = Encoding.ASCII - .GetString(Convert.FromBase64String(token)) - .Split(Separator); - if (credentials.Length != 2) - return null; - - var username = ApiClient.DecodeTokenElement(credentials[0]); - var password = ApiClient.DecodeTokenElement(credentials[1]); - - var providerKey = UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider; - var provider = Membership.Providers[providerKey]; - if (provider == null || !provider.ValidateUser(username, password)) - return null; - var user = Current.Services.UserService.GetByUsername(username); - if (!user.IsApproved || user.IsLockedOut) - return null; - return user; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Api/ApiClient.cs b/src/Umbraco.ModelsBuilder/Api/ApiClient.cs deleted file mode 100644 index dde3641b97..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ApiClient.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Net.Http.Formatting; -using System.Net.Http.Headers; -using System.Text; - -namespace Umbraco.ModelsBuilder.Api -{ - public class ApiClient - { - private readonly string _url; - private readonly string _user; - private readonly string _password; - - private readonly JsonMediaTypeFormatter _formatter; - private readonly MediaTypeFormatter[] _formatters; - - // fixme hardcoded? - // could be options - but we cannot "discover" them as the API client runs outside of the web app - // in addition, anything that references the controller forces API clients to reference Umbraco.Core - private const string ApiControllerUrl = "/Umbraco/BackOffice/ModelsBuilder/ModelsBuilderApi/"; - - public ApiClient(string url, string user, string password) - { - _url = url.TrimEnd('/'); - _user = user; - _password = password; - - _formatter = new JsonMediaTypeFormatter(); - _formatters = new MediaTypeFormatter[] { _formatter }; - } - - private void SetBaseAddress(HttpClient client, string url) - { - try - { - client.BaseAddress = new Uri(url); - } - catch - { - throw new UriFormatException($"Invalid URI: the format of the URI \"{url}\" could not be determined."); - } - } - - public void ValidateClientVersion() - { - // FIXME - add proxys support - - var hch = new HttpClientHandler(); - - using (var client = new HttpClient(hch)) - { - SetBaseAddress(client, _url); - Authorize(client); - - var data = new ValidateClientVersionData - { - ClientVersion = ApiVersion.Current.Version, - MinServerVersionSupportingClient = ApiVersion.Current.MinServerVersionSupportingClient, - }; - - var result = client.PostAsync(_url + ApiControllerUrl + nameof(ModelsBuilderApiController.ValidateClientVersion), - data, _formatter).Result; - - // this is not providing enough details in case of an error - do our own reporting - //result.EnsureSuccessStatusCode(); - EnsureSuccess(result); - } - } - - public IDictionary GetModels(Dictionary ourFiles, string modelsNamespace) - { - // FIXME - add proxys support - - var hch = new HttpClientHandler(); - - //hch.Proxy = new WebProxy("path.to.proxy", 8888); - //hch.UseProxy = true; - - using (var client = new HttpClient(hch)) - { - SetBaseAddress(client, _url); - Authorize(client); - - var data = new GetModelsData - { - Namespace = modelsNamespace, - ClientVersion = ApiVersion.Current.Version, - MinServerVersionSupportingClient = ApiVersion.Current.MinServerVersionSupportingClient, - Files = ourFiles - }; - - var result = client.PostAsync(_url + ApiControllerUrl + nameof(ModelsBuilderApiController.GetModels), - data, _formatter).Result; - - // this is not providing enough details in case of an error - do our own reporting - //result.EnsureSuccessStatusCode(); - EnsureSuccess(result); - - var genFiles = result.Content.ReadAsAsync>(_formatters).Result; - return genFiles; - } - } - - private static void EnsureSuccess(HttpResponseMessage result) - { - if (result.IsSuccessStatusCode) return; - - var text = result.Content.ReadAsStringAsync().Result; - throw new Exception($"Response status code does not indicate success ({result.StatusCode})\n{text}"); - } - - private void Authorize(HttpClient client) - { - AuthorizeBasic(client); - } - - // fixme - for the time being, we don't cache the token and we auth on each API call - // not used at the moment - /* - private void AuthorizeIdentity(HttpClient client) - { - var formData = new FormUrlEncodedContent(new[] - { - new KeyValuePair("grant_type", "password"), - new KeyValuePair("userName", _user), - new KeyValuePair("password", _password), - }); - - var result = client.PostAsync(_url + UmbracoOAuthTokenUrl, formData).Result; - - EnsureSuccess(result); - - var token = result.Content.ReadAsAsync(_formatters).Result; - if (token.TokenType != "bearer") - throw new Exception($"Received invalid token type \"{token.TokenType}\", expected \"bearer\"."); - - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); - } - */ - - private void AuthorizeBasic(HttpClient client) - { - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", - Convert.ToBase64String(Encoding.UTF8.GetBytes(EncodeTokenElement(_user) + ':' + EncodeTokenElement(_password)))); - } - - public static string EncodeTokenElement(string s) - { - return s.Replace("%", "%a").Replace(":", "%b"); - } - - public static string DecodeTokenElement(string s) - { - return s.Replace("%b", ":").Replace("%a", "%"); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Api/ApiHelper.cs b/src/Umbraco.ModelsBuilder/Api/ApiHelper.cs deleted file mode 100644 index fa6492fe3f..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ApiHelper.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using System.Text; -using Umbraco.ModelsBuilder.Building; -using Umbraco.ModelsBuilder.Umbraco; - -namespace Umbraco.ModelsBuilder.Api -{ - internal static class ApiHelper - { - public static Dictionary GetModels(UmbracoServices umbracoServices, string modelsNamespace, IDictionary files) - { - var typeModels = umbracoServices.GetAllTypes(); - - var parseResult = new CodeParser().ParseWithReferencedAssemblies(files); - var builder = new TextBuilder(typeModels, parseResult, modelsNamespace); - - var models = new Dictionary(); - foreach (var typeModel in builder.GetModelsToGenerate()) - { - var sb = new StringBuilder(); - builder.Generate(sb, typeModel); - models[typeModel.ClrName] = sb.ToString(); - } - return models; - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Api/ApiVersion.cs b/src/Umbraco.ModelsBuilder/Api/ApiVersion.cs deleted file mode 100644 index 2ee64b8c54..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ApiVersion.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Reflection; - -namespace Umbraco.ModelsBuilder.Api -{ - /// - /// Manages API version handshake between client and server. - /// - public class ApiVersion - { - #region Configure - - // indicate the minimum version of the client API that is supported by this server's API. - // eg our Version = 4.8 but we support connections from VSIX down to version 3.2 - // => as a server, we accept connections from client down to version ... - private static readonly Version MinClientVersionSupportedByServerConst = new Version(3, 0, 0, 0); - - // indicate the minimum version of the server that can support the client API - // eg our Version = 4.8 and we know we're compatible with website server down to version 3.2 - // => as a client, we tell the server down to version ... that it should accept us - private static readonly Version MinServerVersionSupportingClientConst = new Version(3, 0, 0, 0); - - #endregion - - /// - /// Initializes a new instance of the class. - /// - /// The currently executing version. - /// The min client version supported by the server. - /// An opt min server version supporting the client. - internal ApiVersion(Version executingVersion, Version minClientVersionSupportedByServer, Version minServerVersionSupportingClient = null) - { - if (executingVersion == null) throw new ArgumentNullException(nameof(executingVersion)); - if (minClientVersionSupportedByServer == null) throw new ArgumentNullException(nameof(minClientVersionSupportedByServer)); - - Version = executingVersion; - MinClientVersionSupportedByServer = minClientVersionSupportedByServer; - MinServerVersionSupportingClient = minServerVersionSupportingClient; - } - - /// - /// Gets the currently executing API version. - /// - public static ApiVersion Current { get; } - = new ApiVersion(Assembly.GetExecutingAssembly().GetName().Version, - MinClientVersionSupportedByServerConst, MinServerVersionSupportingClientConst); - - /// - /// Gets the executing version of the API. - /// - public Version Version { get; } - - /// - /// Gets the min client version supported by the server. - /// - public Version MinClientVersionSupportedByServer { get; } - - /// - /// Gets the min server version supporting the client. - /// - public Version MinServerVersionSupportingClient { get; } - - /// - /// Gets a value indicating whether the API server is compatible with a client. - /// - /// The client version. - /// An opt min server version supporting the client. - /// - /// A client is compatible with a server if the client version is greater-or-equal _minClientVersionSupportedByServer - /// (ie client can be older than server, up to a point) AND the client version is lower-or-equal the server version - /// (ie client cannot be more recent than server) UNLESS the server . - /// - public bool IsCompatibleWith(Version clientVersion, Version minServerVersionSupportingClient = null) - { - // client cannot be older than server's min supported version - if (clientVersion < MinClientVersionSupportedByServer) - return false; - - // if we know about this client (client is older than server), it is supported - if (clientVersion <= Version) // if we know about this client (client older than server) - return true; - - // if we don't know about this client (client is newer than server), - // give server a chance to tell client it is, indeed, ok to support it - return minServerVersionSupportingClient != null && minServerVersionSupportingClient <= Version; - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Api/GetModelsData.cs b/src/Umbraco.ModelsBuilder/Api/GetModelsData.cs deleted file mode 100644 index 9a5c55afc2..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/GetModelsData.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using System.Runtime.Serialization; - -namespace Umbraco.ModelsBuilder.Api -{ - [DataContract] - public class GetModelsData : ValidateClientVersionData - { - [DataMember] - public string Namespace { get; set; } - - [DataMember] - public IDictionary Files { get; set; } - - public override bool IsValid => base.IsValid && Files != null; - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Api/ModelsBuilderApiController.cs b/src/Umbraco.ModelsBuilder/Api/ModelsBuilderApiController.cs deleted file mode 100644 index 444910b069..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ModelsBuilderApiController.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.ModelsBuilder.Umbraco; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; - -namespace Umbraco.ModelsBuilder.Api -{ - // read http://umbraco.com/follow-us/blog-archive/2014/1/17/heads-up,-breaking-change-coming-in-702-and-62.aspx - // read http://our.umbraco.org/forum/developers/api-questions/43025-Web-API-authentication - // UmbracoAuthorizedApiController :: /Umbraco/BackOffice/Zbu/ModelsBuilderApi/GetTypeModels - // UmbracoApiController :: /Umbraco/Zbu/ModelsBuilderApi/GetTypeModels ?? UNLESS marked with isbackoffice - // - // BEWARE! the controller url is hard-coded in ModelsBuilderApi and needs to be in sync! - - [PluginController(ControllerArea)] - [IsBackOffice] - //[UmbracoApplicationAuthorize(Constants.Applications.Developer)] // see ApiBasicAuthFilter - that one would be for ASP.NET identity - public class ModelsBuilderApiController : UmbracoApiController // UmbracoAuthorizedApiController - for ASP.NET identity - { - public const string ControllerArea = "ModelsBuilder"; - - private readonly UmbracoServices _umbracoServices; - - public ModelsBuilderApiController(UmbracoServices umbracoServices) - { - _umbracoServices = umbracoServices; - } - - // invoked by the API - [System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers! - [ApiBasicAuthFilter("developer")] // have to use our own, non-cookie-based, auth - public HttpResponseMessage ValidateClientVersion(ValidateClientVersionData data) - { - if (!UmbracoConfig.For.ModelsBuilder().ApiServer) - return Request.CreateResponse(HttpStatusCode.Forbidden, "API server does not want to talk to you."); - - if (!ModelState.IsValid || data == null || !data.IsValid) - return Request.CreateResponse(HttpStatusCode.BadRequest, "Invalid data."); - - var checkResult = CheckVersion(data.ClientVersion, data.MinServerVersionSupportingClient); - return (checkResult.Success - ? Request.CreateResponse(HttpStatusCode.OK, "OK", Configuration.Formatters.JsonFormatter) - : checkResult.Result); - } - - // invoked by the API - [System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers! - [ApiBasicAuthFilter("developer")] // have to use our own, non-cookie-based, auth - public HttpResponseMessage GetModels(GetModelsData data) - { - if (!UmbracoConfig.For.ModelsBuilder().ApiServer) - return Request.CreateResponse(HttpStatusCode.Forbidden, "API server does not want to talk to you."); - - if (!ModelState.IsValid || data == null || !data.IsValid) - return Request.CreateResponse(HttpStatusCode.BadRequest, "Invalid data."); - - var checkResult = CheckVersion(data.ClientVersion, data.MinServerVersionSupportingClient); - if (!checkResult.Success) - return checkResult.Result; - - var models = ApiHelper.GetModels(_umbracoServices, data.Namespace, data.Files); - - return Request.CreateResponse(HttpStatusCode.OK, models, Configuration.Formatters.JsonFormatter); - } - - private Attempt CheckVersion(Version clientVersion, Version minServerVersionSupportingClient) - { - if (clientVersion == null) - return Attempt.Fail(Request.CreateResponse(HttpStatusCode.Forbidden, - $"API version conflict: client version () is not compatible with server version({ApiVersion.Current.Version}).")); - - // minServerVersionSupportingClient can be null - var isOk = ApiVersion.Current.IsCompatibleWith(clientVersion, minServerVersionSupportingClient); - var response = isOk ? null : Request.CreateResponse(HttpStatusCode.Forbidden, - $"API version conflict: client version ({clientVersion}) is not compatible with server version({ApiVersion.Current.Version})."); - - return Attempt.If(isOk, response); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Api/TokenData.cs b/src/Umbraco.ModelsBuilder/Api/TokenData.cs deleted file mode 100644 index c34a6c75c5..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/TokenData.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Runtime.Serialization; - -namespace Umbraco.ModelsBuilder.Api -{ - [DataContract] - class TokenData - { - [DataMember(Name = "access_token")] - public string AccessToken { get; set; } - - [DataMember(Name = "token_type")] - public string TokenType { get; set; } - - [DataMember(Name = "expires_in")] - public int ExpiresIn { get; set; } - } -} diff --git a/src/Umbraco.ModelsBuilder/Api/ValidateClientVersionData.cs b/src/Umbraco.ModelsBuilder/Api/ValidateClientVersionData.cs deleted file mode 100644 index 39ef08d816..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ValidateClientVersionData.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Runtime.Serialization; - -namespace Umbraco.ModelsBuilder.Api -{ - [DataContract] - public class ValidateClientVersionData - { - // issues 32, 34... problems when serializing versions - // - // make sure System.Version objects are transfered as strings - // depending on the JSON serializer version, it looks like versions are causing issues - // see - // http://stackoverflow.com/questions/13170386/why-system-version-in-json-string-does-not-deserialize-correctly - // - // if the class is marked with [DataContract] then only properties marked with [DataMember] - // are serialized and the rest is ignored, see - // http://www.asp.net/web-api/overview/formats-and-model-binding/json-and-xml-serialization - - [DataMember] - public string ClientVersionString - { - get { return VersionToString(ClientVersion); } - set { ClientVersion = ParseVersion(value, false, "client"); } - } - - [DataMember] - public string MinServerVersionSupportingClientString - { - get { return VersionToString(MinServerVersionSupportingClient); } - set { MinServerVersionSupportingClient = ParseVersion(value, true, "minServer"); } - } - - // not serialized - public Version ClientVersion { get; set; } - public Version MinServerVersionSupportingClient { get; set; } - - private static string VersionToString(Version version) - { - return version?.ToString() ?? "0.0.0.0"; - } - - private static Version ParseVersion(string value, bool canBeNull, string name) - { - if (string.IsNullOrWhiteSpace(value) && canBeNull) - return null; - - Version version; - if (Version.TryParse(value, out version)) - return version; - - throw new ArgumentException($"Failed to parse \"{value}\" as {name} version."); - } - - public virtual bool IsValid => ClientVersion != null; - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Building/CodeDomBuilder.cs b/src/Umbraco.ModelsBuilder/Building/CodeDomBuilder.cs deleted file mode 100644 index 925337bd1e..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/CodeDomBuilder.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.CodeDom; -using System.Collections.Generic; - -namespace Umbraco.ModelsBuilder.Building -{ - // NOTE - // See nodes in Builder.cs class - that one does not work, is not complete, - // and was just some sort of experiment... - - /// - /// Implements a builder that works by using CodeDom - /// - internal class CodeDomBuilder : Builder - { - /// - /// Initializes a new instance of the class with a list of models to generate. - /// - /// The list of models to generate. - public CodeDomBuilder(IList typeModels) - : base(typeModels, null) - { } - - /// - /// Outputs a generated model to a code namespace. - /// - /// The code namespace. - /// The model to generate. - public void Generate(CodeNamespace ns, TypeModel typeModel) - { - // what about USING? - // what about references? - - if (typeModel.IsMixin) - { - var i = new CodeTypeDeclaration("I" + typeModel.ClrName) - { - IsInterface = true, - IsPartial = true, - Attributes = MemberAttributes.Public - }; - i.BaseTypes.Add(typeModel.BaseType == null ? "IPublishedContent" : "I" + typeModel.BaseType.ClrName); - - foreach (var mixinType in typeModel.DeclaringInterfaces) - i.BaseTypes.Add(mixinType.ClrName); - - i.Comments.Add(new CodeCommentStatement($"Mixin content Type {typeModel.Id} with alias \"{typeModel.Alias}\"")); - - foreach (var propertyModel in typeModel.Properties) - { - var p = new CodeMemberProperty - { - Name = propertyModel.ClrName, - Type = new CodeTypeReference(propertyModel.ModelClrType), - Attributes = MemberAttributes.Public, - HasGet = true, - HasSet = false - }; - i.Members.Add(p); - } - } - - var c = new CodeTypeDeclaration(typeModel.ClrName) - { - IsClass = true, - IsPartial = true, - Attributes = MemberAttributes.Public - }; - - c.BaseTypes.Add(typeModel.BaseType == null ? "PublishedContentModel" : typeModel.BaseType.ClrName); - - // if it's a missing it implements its own interface - if (typeModel.IsMixin) - c.BaseTypes.Add("I" + typeModel.ClrName); - - // write the mixins, if any, as interfaces - // only if not a mixin because otherwise the interface already has them - if (typeModel.IsMixin == false) - foreach (var mixinType in typeModel.DeclaringInterfaces) - c.BaseTypes.Add("I" + mixinType.ClrName); - - foreach (var mixin in typeModel.MixinTypes) - c.BaseTypes.Add("I" + mixin.ClrName); - - c.Comments.Add(new CodeCommentStatement($"Content Type {typeModel.Id} with alias \"{typeModel.Alias}\"")); - - foreach (var propertyModel in typeModel.Properties) - { - var p = new CodeMemberProperty - { - Name = propertyModel.ClrName, - Type = new CodeTypeReference(propertyModel.ModelClrType), - Attributes = MemberAttributes.Public, - HasGet = true, - HasSet = false - }; - p.GetStatements.Add(new CodeMethodReturnStatement( // return - new CodeMethodInvokeExpression( - new CodeMethodReferenceExpression( - new CodeThisReferenceExpression(), // this - "Value", // .Value - new[] // - { - new CodeTypeReference(propertyModel.ModelClrType) - }), - new CodeExpression[] // ("alias") - { - new CodePrimitiveExpression(propertyModel.Alias) - }))); - c.Members.Add(p); - } - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/CodeParser.cs b/src/Umbraco.ModelsBuilder/Building/CodeParser.cs deleted file mode 100644 index 30fcbf1f91..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/CodeParser.cs +++ /dev/null @@ -1,238 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Umbraco.Core.Models.PublishedContent; - -namespace Umbraco.ModelsBuilder.Building -{ - /// - /// Implements code parsing. - /// - /// Parses user's code and look for generator's instructions. - internal class CodeParser - { - /// - /// Parses a set of file. - /// - /// A set of (filename,content) representing content to parse. - /// The result of the code parsing. - /// The set of files is a dictionary of name, content. - public ParseResult Parse(IDictionary files) - { - return Parse(files, Enumerable.Empty()); - } - - /// - /// Parses a set of file. - /// - /// A set of (filename,content) representing content to parse. - /// Assemblies to reference in compilations. - /// The result of the code parsing. - /// The set of files is a dictionary of name, content. - public ParseResult Parse(IDictionary files, IEnumerable references) - { - SyntaxTree[] trees; - var compiler = new Compiler { References = references }; - var compilation = compiler.GetCompilation("Umbraco.ModelsBuilder.Generated", files, out trees); - - var disco = new ParseResult(); - foreach (var tree in trees) - Parse(disco, compilation, tree); - - return disco; - } - - public ParseResult ParseWithReferencedAssemblies(IDictionary files) - { - return Parse(files, ReferencedAssemblies.References); - } - - private static void Parse(ParseResult disco, CSharpCompilation compilation, SyntaxTree tree) - { - var model = compilation.GetSemanticModel(tree); - - //we quite probably have errors but that is normal - //var diags = model.GetDiagnostics(); - - var classDecls = tree.GetRoot().DescendantNodes().OfType(); - foreach (var classSymbol in classDecls.Select(x => model.GetDeclaredSymbol(x))) - { - ParseClassSymbols(disco, classSymbol); - - var baseClassSymbol = classSymbol.BaseType; - if (baseClassSymbol != null) - //disco.SetContentBaseClass(SymbolDisplay.ToDisplayString(classSymbol), SymbolDisplay.ToDisplayString(baseClassSymbol)); - disco.SetContentBaseClass(classSymbol.Name, baseClassSymbol.Name); - - var interfaceSymbols = classSymbol.Interfaces; - disco.SetContentInterfaces(classSymbol.Name, //SymbolDisplay.ToDisplayString(classSymbol), - interfaceSymbols.Select(x => x.Name)); //SymbolDisplay.ToDisplayString(x))); - - var hasCtor = classSymbol.Constructors - .Any(x => - { - if (x.IsStatic) return false; - if (x.Parameters.Length != 1) return false; - var type1 = x.Parameters[0].Type; - var type2 = typeof (IPublishedContent); - return type1.ToDisplayString() == type2.FullName; - }); - - if (hasCtor) - disco.SetHasCtor(classSymbol.Name); - - foreach (var propertySymbol in classSymbol.GetMembers().Where(x => x is IPropertySymbol)) - ParsePropertySymbols(disco, classSymbol, propertySymbol); - - foreach (var staticMethodSymbol in classSymbol.GetMembers().Where(x => x is IMethodSymbol)) - ParseMethodSymbol(disco, classSymbol, staticMethodSymbol); - } - - var interfaceDecls = tree.GetRoot().DescendantNodes().OfType(); - foreach (var interfaceSymbol in interfaceDecls.Select(x => model.GetDeclaredSymbol(x))) - { - ParseClassSymbols(disco, interfaceSymbol); - - var interfaceSymbols = interfaceSymbol.Interfaces; - disco.SetContentInterfaces(interfaceSymbol.Name, //SymbolDisplay.ToDisplayString(interfaceSymbol), - interfaceSymbols.Select(x => x.Name)); // SymbolDisplay.ToDisplayString(x))); - } - - ParseAssemblySymbols(disco, compilation.Assembly); - } - - private static void ParseClassSymbols(ParseResult disco, ISymbol symbol) - { - foreach (var attrData in symbol.GetAttributes()) - { - var attrClassSymbol = attrData.AttributeClass; - - // handle errors - if (attrClassSymbol is IErrorTypeSymbol) continue; - if (attrData.AttributeConstructor == null) continue; - - var attrClassName = SymbolDisplay.ToDisplayString(attrClassSymbol); - switch (attrClassName) - { - case "Umbraco.ModelsBuilder.IgnorePropertyTypeAttribute": - var propertyAliasToIgnore = (string)attrData.ConstructorArguments[0].Value; - disco.SetIgnoredProperty(symbol.Name /*SymbolDisplay.ToDisplayString(symbol)*/, propertyAliasToIgnore); - break; - case "Umbraco.ModelsBuilder.RenamePropertyTypeAttribute": - var propertyAliasToRename = (string)attrData.ConstructorArguments[0].Value; - var propertyRenamed = (string)attrData.ConstructorArguments[1].Value; - disco.SetRenamedProperty(symbol.Name /*SymbolDisplay.ToDisplayString(symbol)*/, propertyAliasToRename, propertyRenamed); - break; - // that one causes all sorts of issues with references to Umbraco.Core in Roslyn - //case "Umbraco.Core.Models.PublishedContent.PublishedContentModelAttribute": - // var contentAliasToRename = (string)attrData.ConstructorArguments[0].Value; - // disco.SetRenamedContent(contentAliasToRename, symbol.Name /*SymbolDisplay.ToDisplayString(symbol)*/); - // break; - case "Umbraco.ModelsBuilder.ImplementContentTypeAttribute": - var contentAliasToRename = (string)attrData.ConstructorArguments[0].Value; - disco.SetRenamedContent(contentAliasToRename, symbol.Name, true /*SymbolDisplay.ToDisplayString(symbol)*/); - break; - } - } - } - - private static void ParsePropertySymbols(ParseResult disco, ISymbol classSymbol, ISymbol symbol) - { - foreach (var attrData in symbol.GetAttributes()) - { - var attrClassSymbol = attrData.AttributeClass; - - // handle errors - if (attrClassSymbol is IErrorTypeSymbol) continue; - if (attrData.AttributeConstructor == null) continue; - - var attrClassName = SymbolDisplay.ToDisplayString(attrClassSymbol); - // ReSharper disable once SwitchStatementMissingSomeCases - switch (attrClassName) - { - case "Umbraco.ModelsBuilder.ImplementPropertyTypeAttribute": - var propertyAliasToIgnore = (string)attrData.ConstructorArguments[0].Value; - disco.SetIgnoredProperty(classSymbol.Name /*SymbolDisplay.ToDisplayString(classSymbol)*/, propertyAliasToIgnore); - break; - } - } - } - - private static void ParseAssemblySymbols(ParseResult disco, ISymbol symbol) - { - foreach (var attrData in symbol.GetAttributes()) - { - var attrClassSymbol = attrData.AttributeClass; - - // handle errors - if (attrClassSymbol is IErrorTypeSymbol) continue; - if (attrData.AttributeConstructor == null) continue; - - var attrClassName = SymbolDisplay.ToDisplayString(attrClassSymbol); - switch (attrClassName) - { - case "Umbraco.ModelsBuilder.IgnoreContentTypeAttribute": - var contentAliasToIgnore = (string)attrData.ConstructorArguments[0].Value; - // see notes in IgnoreContentTypeAttribute - //var ignoreContent = (bool)attrData.ConstructorArguments[1].Value; - //var ignoreMixin = (bool)attrData.ConstructorArguments[1].Value; - //var ignoreMixinProperties = (bool)attrData.ConstructorArguments[1].Value; - disco.SetIgnoredContent(contentAliasToIgnore /*, ignoreContent, ignoreMixin, ignoreMixinProperties*/); - break; - - case "Umbraco.ModelsBuilder.RenameContentTypeAttribute": - var contentAliasToRename = (string) attrData.ConstructorArguments[0].Value; - var contentRenamed = (string)attrData.ConstructorArguments[1].Value; - disco.SetRenamedContent(contentAliasToRename, contentRenamed, false); - break; - - case "Umbraco.ModelsBuilder.ModelsBaseClassAttribute": - var modelsBaseClass = (INamedTypeSymbol) attrData.ConstructorArguments[0].Value; - if (modelsBaseClass is IErrorTypeSymbol) - throw new Exception($"Invalid base class type \"{modelsBaseClass.Name}\"."); - disco.SetModelsBaseClassName(SymbolDisplay.ToDisplayString(modelsBaseClass)); - break; - - case "Umbraco.ModelsBuilder.ModelsNamespaceAttribute": - var modelsNamespace= (string) attrData.ConstructorArguments[0].Value; - disco.SetModelsNamespace(modelsNamespace); - break; - - case "Umbraco.ModelsBuilder.ModelsUsingAttribute": - var usingNamespace = (string)attrData.ConstructorArguments[0].Value; - disco.SetUsingNamespace(usingNamespace); - break; - } - } - } - - private static void ParseMethodSymbol(ParseResult disco, ISymbol classSymbol, ISymbol symbol) - { - var methodSymbol = symbol as IMethodSymbol; - - if (methodSymbol == null - || !methodSymbol.IsStatic - || methodSymbol.IsGenericMethod - || methodSymbol.ReturnsVoid - || methodSymbol.IsExtensionMethod - || methodSymbol.Parameters.Length != 1) - return; - - var returnType = methodSymbol.ReturnType; - var paramSymbol = methodSymbol.Parameters[0]; - var paramType = paramSymbol.Type; - - // cannot do this because maybe the param type is ISomething and we don't have - // that type yet - will be generated - so cannot put any condition on it really - //const string iPublishedContent = "Umbraco.Core.Models.IPublishedContent"; - //var implements = paramType.AllInterfaces.Any(x => x.ToDisplayString() == iPublishedContent); - //if (!implements) - // return; - - disco.SetStaticMixinMethod(classSymbol.Name, methodSymbol.Name, returnType.Name, paramType.Name); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/Compiler.cs b/src/Umbraco.ModelsBuilder/Building/Compiler.cs deleted file mode 100644 index 66064bef0b..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/Compiler.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Web; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; - -namespace Umbraco.ModelsBuilder.Building -{ - // main Roslyn compiler - internal class Compiler - { - private readonly LanguageVersion _languageVersion; - - public Compiler() - : this(UmbracoConfig.For.ModelsBuilder().LanguageVersion) - { } - - public Compiler(LanguageVersion languageVersion) - { - _languageVersion = languageVersion; - References = ReferencedAssemblies.References; - Debug = HttpContext.Current != null && HttpContext.Current.IsDebuggingEnabled; - } - - // gets or sets the references - public IEnumerable References { get; set; } - - public bool Debug { get; set; } - - // gets a compilation - public CSharpCompilation GetCompilation(string assemblyName, IDictionary files) - { - SyntaxTree[] trees; - return GetCompilation(assemblyName, files, out trees); - } - - // gets a compilation - // used by CodeParser to get a "compilation" of the existing files - public CSharpCompilation GetCompilation(string assemblyName, IDictionary files, out SyntaxTree[] trees) - { - var options = new CSharpParseOptions(_languageVersion); - trees = files.Select(x => - { - var text = x.Value; - var tree = CSharpSyntaxTree.ParseText(text, /*options:*/ options); - var diagnostic = tree.GetDiagnostics().FirstOrDefault(y => y.Severity == DiagnosticSeverity.Error); - if (diagnostic != null) - ThrowExceptionFromDiagnostic(x.Key, x.Value, diagnostic); - return tree; - }).ToArray(); - - var refs = References; - - var compilationOptions = new CSharpCompilationOptions( - OutputKind.DynamicallyLinkedLibrary, - assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default, - optimizationLevel: Debug ? OptimizationLevel.Debug : OptimizationLevel.Release - ); - var compilation = CSharpCompilation.Create( - assemblyName, - /*syntaxTrees:*/ trees, - /*references:*/ refs, - compilationOptions); - - return compilation; - } - - // compile files into a Dll - // used by ModelsBuilderBackOfficeController in [Live]Dll mode, to compile the models to disk - public void Compile(string assemblyName, IDictionary files, string binPath) - { - var assemblyPath = Path.Combine(binPath, assemblyName + ".dll"); - using (var stream = new FileStream(assemblyPath, FileMode.Create)) - { - Compile(assemblyName, files, stream); - } - - // this is how we'd create the pdb: - /* - var pdbPath = Path.Combine(binPath, assemblyName + ".pdb"); - - // create the compilation - var compilation = GetCompilation(assemblyName, files); - - // check diagnostics for errors (not warnings) - foreach (var diag in compilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error)) - ThrowExceptionFromDiagnostic(files, diag); - - // emit - var result = compilation.Emit(assemblyPath, pdbPath); - if (result.Success) return; - - // deal with errors - var diagnostic = result.Diagnostics.First(x => x.Severity == DiagnosticSeverity.Error); - ThrowExceptionFromDiagnostic(files, diagnostic); - */ - } - - // compile files into an assembly - public Assembly Compile(string assemblyName, IDictionary files) - { - using (var stream = new MemoryStream()) - { - Compile(assemblyName, files, stream); - return Assembly.Load(stream.GetBuffer()); - } - } - - // compile one file into an assembly - public Assembly Compile(string assemblyName, string path, string code) - { - using (var stream = new MemoryStream()) - { - Compile(assemblyName, new Dictionary { { path, code } }, stream); - return Assembly.Load(stream.GetBuffer()); - } - } - - // compiles files into a stream - public void Compile(string assemblyName, IDictionary files, Stream stream) - { - // create the compilation - var compilation = GetCompilation(assemblyName, files); - - // check diagnostics for errors (not warnings) - foreach (var diag in compilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error)) - ThrowExceptionFromDiagnostic(files, diag); - - // emit - var result = compilation.Emit(stream); - if (result.Success) return; - - // deal with errors - var diagnostic = result.Diagnostics.First(x => x.Severity == DiagnosticSeverity.Error); - ThrowExceptionFromDiagnostic(files, diagnostic); - } - - // compiles one file into a stream - public void Compile(string assemblyName, string path, string code, Stream stream) - { - Compile(assemblyName, new Dictionary { { path, code } }, stream); - } - - private static void ThrowExceptionFromDiagnostic(IDictionary files, Diagnostic diagnostic) - { - var message = diagnostic.GetMessage(); - if (diagnostic.Location == Location.None) - throw new CompilerException(message); - - var position = diagnostic.Location.GetLineSpan().StartLinePosition.Line + 1; - var path = diagnostic.Location.SourceTree.FilePath; - var code = files.ContainsKey(path) ? files[path] : string.Empty; - throw new CompilerException(message, path, code, position); - } - - private static void ThrowExceptionFromDiagnostic(string path, string code, Diagnostic diagnostic) - { - var message = diagnostic.GetMessage(); - if (diagnostic.Location == Location.None) - throw new CompilerException(message); - - var position = diagnostic.Location.GetLineSpan().StartLinePosition.Line + 1; - throw new CompilerException(message, path, code, position); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/CompilerException.cs b/src/Umbraco.ModelsBuilder/Building/CompilerException.cs deleted file mode 100644 index e978f67ae5..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/CompilerException.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder.Building -{ - public class CompilerException : Exception - { - public CompilerException(string message) - : base(message) - { } - - public CompilerException(string message, string path, string sourceCode, int line) - : base(message) - { - Path = path; - SourceCode = sourceCode; - Line = line; - } - - public string Path { get; } = string.Empty; - - public string SourceCode { get; } = string.Empty; - - public int Line { get; } = -1; - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/ParseResult.cs b/src/Umbraco.ModelsBuilder/Building/ParseResult.cs deleted file mode 100644 index d1f61363ff..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/ParseResult.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Umbraco.ModelsBuilder.Building -{ - /// - /// Contains the result of a code parsing. - /// - internal class ParseResult - { - // "alias" is the umbraco alias - // content "name" is the complete name eg Foo.Bar.Name - // property "name" is just the local name - - // see notes in IgnoreContentTypeAttribute - - private readonly HashSet _ignoredContent - = new HashSet(StringComparer.InvariantCultureIgnoreCase); - //private readonly HashSet _ignoredMixin - // = new HashSet(StringComparer.InvariantCultureIgnoreCase); - //private readonly HashSet _ignoredMixinProperties - // = new HashSet(StringComparer.InvariantCultureIgnoreCase); - private readonly Dictionary _renamedContent - = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - private readonly HashSet _withImplementContent - = new HashSet(StringComparer.InvariantCultureIgnoreCase); - private readonly Dictionary> _ignoredProperty - = new Dictionary>(); - private readonly Dictionary> _renamedProperty - = new Dictionary>(); - private readonly Dictionary _contentBase - = new Dictionary(); - private readonly Dictionary _contentInterfaces - = new Dictionary(); - private readonly List _usingNamespaces - = new List(); - private readonly Dictionary> _staticMixins - = new Dictionary>(); - private readonly HashSet _withCtor - = new HashSet(StringComparer.InvariantCultureIgnoreCase); - - public static readonly ParseResult Empty = new ParseResult(); - - private class StaticMixinMethodInfo - { - public StaticMixinMethodInfo(string contentName, string methodName, string returnType, string paramType) - { - ContentName = contentName; - MethodName = methodName; - //ReturnType = returnType; - //ParamType = paramType; - } - - // short name eg Type1 - public string ContentName { get; private set; } - - // short name eg GetProp1 - public string MethodName { get; private set; } - - // those types cannot be FQ because when parsing, some of them - // might not exist since we're generating them... and so prob. - // that info is worthless - not using it anyway at the moment... - - //public string ReturnType { get; private set; } - //public string ParamType { get; private set; } - } - - #region Declare - - // content with that alias should not be generated - // alias can end with a * (wildcard) - public void SetIgnoredContent(string contentAlias /*, bool ignoreContent, bool ignoreMixin, bool ignoreMixinProperties*/) - { - //if (ignoreContent) - _ignoredContent.Add(contentAlias); - //if (ignoreMixin) - // _ignoredMixin.Add(contentAlias); - //if (ignoreMixinProperties) - // _ignoredMixinProperties.Add(contentAlias); - } - - // content with that alias should be generated with a different name - public void SetRenamedContent(string contentAlias, string contentName, bool withImplement) - { - _renamedContent[contentAlias] = contentName; - if (withImplement) - _withImplementContent.Add(contentAlias); - } - - // property with that alias should not be generated - // applies to content name and any content that implements it - // here, contentName may be an interface - // alias can end with a * (wildcard) - public void SetIgnoredProperty(string contentName, string propertyAlias) - { - HashSet ignores; - if (!_ignoredProperty.TryGetValue(contentName, out ignores)) - ignores = _ignoredProperty[contentName] = new HashSet(StringComparer.InvariantCultureIgnoreCase); - ignores.Add(propertyAlias); - } - - // property with that alias should be generated with a different name - // applies to content name and any content that implements it - // here, contentName may be an interface - public void SetRenamedProperty(string contentName, string propertyAlias, string propertyName) - { - Dictionary renames; - if (!_renamedProperty.TryGetValue(contentName, out renames)) - renames = _renamedProperty[contentName] = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - renames[propertyAlias] = propertyName; - } - - // content with that name has a base class so no need to generate one - public void SetContentBaseClass(string contentName, string baseName) - { - if (baseName.ToLowerInvariant() != "object") - _contentBase[contentName] = baseName; - } - - // content with that name implements the interfaces - public void SetContentInterfaces(string contentName, IEnumerable interfaceNames) - { - _contentInterfaces[contentName] = interfaceNames.ToArray(); - } - - public void SetModelsBaseClassName(string modelsBaseClassName) - { - ModelsBaseClassName = modelsBaseClassName; - } - - public void SetModelsNamespace(string modelsNamespace) - { - ModelsNamespace = modelsNamespace; - } - - public void SetUsingNamespace(string usingNamespace) - { - _usingNamespaces.Add(usingNamespace); - } - - public void SetStaticMixinMethod(string contentName, string methodName, string returnType, string paramType) - { - if (!_staticMixins.ContainsKey(contentName)) - _staticMixins[contentName] = new List(); - - _staticMixins[contentName].Add(new StaticMixinMethodInfo(contentName, methodName, returnType, paramType)); - } - - public void SetHasCtor(string contentName) - { - _withCtor.Add(contentName); - } - - #endregion - - #region Query - - public bool IsIgnored(string contentAlias) - { - return IsContentOrMixinIgnored(contentAlias, _ignoredContent); - } - - //public bool IsMixinIgnored(string contentAlias) - //{ - // return IsContentOrMixinIgnored(contentAlias, _ignoredMixin); - //} - - //public bool IsMixinPropertiesIgnored(string contentAlias) - //{ - // return IsContentOrMixinIgnored(contentAlias, _ignoredMixinProperties); - //} - - private static bool IsContentOrMixinIgnored(string contentAlias, HashSet ignored) - { - if (ignored.Contains(contentAlias)) return true; - return ignored - .Where(x => x.EndsWith("*")) - .Select(x => x.Substring(0, x.Length - 1)) - .Any(x => contentAlias.StartsWith(x, StringComparison.InvariantCultureIgnoreCase)); - } - - public bool HasContentBase(string contentName) - { - return _contentBase.ContainsKey(contentName); - } - - public bool IsContentRenamed(string contentAlias) - { - return _renamedContent.ContainsKey(contentAlias); - } - - public bool HasContentImplement(string contentAlias) - { - return _withImplementContent.Contains(contentAlias); - } - - public string ContentClrName(string contentAlias) - { - string name; - return (_renamedContent.TryGetValue(contentAlias, out name)) ? name : null; - } - - public bool IsPropertyIgnored(string contentName, string propertyAlias) - { - HashSet ignores; - if (_ignoredProperty.TryGetValue(contentName, out ignores)) - { - if (ignores.Contains(propertyAlias)) return true; - if (ignores - .Where(x => x.EndsWith("*")) - .Select(x => x.Substring(0, x.Length - 1)) - .Any(x => propertyAlias.StartsWith(x, StringComparison.InvariantCultureIgnoreCase))) - return true; - } - string baseName; - if (_contentBase.TryGetValue(contentName, out baseName) - && IsPropertyIgnored(baseName, propertyAlias)) return true; - string[] interfaceNames; - if (_contentInterfaces.TryGetValue(contentName, out interfaceNames) - && interfaceNames.Any(interfaceName => IsPropertyIgnored(interfaceName, propertyAlias))) return true; - return false; - } - - public string PropertyClrName(string contentName, string propertyAlias) - { - Dictionary renames; - string name; - if (_renamedProperty.TryGetValue(contentName, out renames) - && renames.TryGetValue(propertyAlias, out name)) return name; - string baseName; - if (_contentBase.TryGetValue(contentName, out baseName) - && null != (name = PropertyClrName(baseName, propertyAlias))) return name; - string[] interfaceNames; - if (_contentInterfaces.TryGetValue(contentName, out interfaceNames) - && null != (name = interfaceNames - .Select(interfaceName => PropertyClrName(interfaceName, propertyAlias)) - .FirstOrDefault(x => x != null))) return name; - return null; - } - - public bool HasModelsBaseClassName - { - get { return !string.IsNullOrWhiteSpace(ModelsBaseClassName); } - } - - public string ModelsBaseClassName { get; private set; } - - public bool HasModelsNamespace - { - get { return !string.IsNullOrWhiteSpace(ModelsNamespace); } - } - - public string ModelsNamespace { get; private set; } - - public IEnumerable UsingNamespaces - { - get { return _usingNamespaces; } - } - - public IEnumerable StaticMixinMethods(string contentName) - { - return _staticMixins.ContainsKey(contentName) - ? _staticMixins[contentName].Select(x => x.MethodName) - : Enumerable.Empty() ; - } - - public bool HasCtor(string contentName) - { - return _withCtor.Contains(contentName); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs b/src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs deleted file mode 100644 index d195846411..0000000000 --- a/src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Umbraco.ModelsBuilder.Configuration -{ - /// - /// Defines the CLR name sources. - /// - public enum ClrNameSource - { - /// - /// No source. - /// - Nothing = 0, - - /// - /// Use the name as source. - /// - Name, - - /// - /// Use the alias as source. - /// - Alias, - - /// - /// Use the alias directly. - /// - RawAlias - } -} diff --git a/src/Umbraco.ModelsBuilder/Configuration/UmbracoConfigExtensions.cs b/src/Umbraco.ModelsBuilder/Configuration/UmbracoConfigExtensions.cs deleted file mode 100644 index acc587e779..0000000000 --- a/src/Umbraco.ModelsBuilder/Configuration/UmbracoConfigExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Threading; -using Umbraco.Core.Configuration; - -namespace Umbraco.ModelsBuilder.Configuration -{ - /// - /// Provides extension methods for the class. - /// - public static class UmbracoConfigExtensions - { - private static Config _config; - - /// - /// Gets the models builder configuration. - /// - /// The umbraco configuration. - /// The models builder configuration. - /// Getting the models builder configuration freezes its state, - /// and any attempt at modifying the configuration using the Setup method - /// will be ignored. - public static Config ModelsBuilder(this UmbracoConfig umbracoConfig) - { - // capture the current Config2.Default value, cannot change anymore - LazyInitializer.EnsureInitialized(ref _config, () => Config.Value); - return _config; - } - - // internal for tests - internal static void ResetConfig() - { - _config = null; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Dashboard/BuilderDashboardHelper.cs b/src/Umbraco.ModelsBuilder/Dashboard/BuilderDashboardHelper.cs deleted file mode 100644 index 9e5741805e..0000000000 --- a/src/Umbraco.ModelsBuilder/Dashboard/BuilderDashboardHelper.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Text; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.ModelsBuilder.Umbraco; - -namespace Umbraco.ModelsBuilder.Dashboard -{ - internal static class BuilderDashboardHelper - { - public static bool CanGenerate() - { - return UmbracoConfig.For.ModelsBuilder().ModelsMode.SupportsExplicitGeneration(); - } - - public static bool GenerateCausesRestart() - { - return UmbracoConfig.For.ModelsBuilder().ModelsMode.IsAnyDll(); - } - - public static bool AreModelsOutOfDate() - { - return OutOfDateModelsStatus.IsOutOfDate; - } - - public static string LastError() - { - return ModelsGenerationError.GetLastError(); - } - - public static string Text() - { - var config = UmbracoConfig.For.ModelsBuilder(); - if (!config.Enable) - return "ModelsBuilder is disabled
(the .Enable key is missing, or its value is not 'true')."; - - var sb = new StringBuilder(); - - sb.Append("ModelsBuilder is enabled, with the following configuration:"); - - sb.Append("
    "); - - sb.Append("
  • The models factory is "); - sb.Append(config.EnableFactory || config.ModelsMode == ModelsMode.PureLive - ? "enabled" - : "not enabled. Umbraco will not use models"); - sb.Append(".
  • "); - - sb.Append("
  • The API is "); - if (config.ApiInstalled && config.EnableApi) - { - sb.Append("installed and enabled"); - if (!config.IsDebug) sb.Append(".
    However, the API runs only with debug compilation mode"); - } - else if (config.ApiInstalled || config.EnableApi) - sb.Append(config.ApiInstalled ? "installed but not enabled" : "enabled but not installed"); - else sb.Append("neither installed nor enabled"); - sb.Append(".
    "); - if (!config.ApiServer) - sb.Append("External tools such as Visual Studio cannot use the API"); - else - sb.Append("The API endpoint is open on this server"); - sb.Append(".
  • "); - - sb.Append(config.ModelsMode != ModelsMode.Nothing - ? $"
  • {config.ModelsMode} models are enabled.
  • " - : "
  • No models mode is specified: models will not be generated.
  • "); - - sb.Append($"
  • Models namespace is {config.ModelsNamespace}.
  • "); - - sb.Append("
  • Static mixin getters are "); - sb.Append(config.StaticMixinGetters ? "enabled" : "disabled"); - if (config.StaticMixinGetters) - { - sb.Append(". The pattern for getters is "); - sb.Append(string.IsNullOrWhiteSpace(config.StaticMixinGetterPattern) - ? "not configured (will use default)" - : $"\"{config.StaticMixinGetterPattern}\""); - } - sb.Append(".
  • "); - - sb.Append("
  • Tracking of out-of-date models is "); - sb.Append(config.FlagOutOfDateModels ? "enabled" : "not enabled"); - sb.Append(".
  • "); - - sb.Append("
"); - - return sb.ToString(); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/EnumerableExtensions.cs b/src/Umbraco.ModelsBuilder/EnumerableExtensions.cs deleted file mode 100644 index da77bfa958..0000000000 --- a/src/Umbraco.ModelsBuilder/EnumerableExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Umbraco.ModelsBuilder -{ - public static class EnumerableExtensions - { - public static void RemoveAll(this IList list, Func predicate) - { - for (var i = 0; i < list.Count; i++) - { - if (predicate(list[i])) - { - list.RemoveAt(i--); // i-- is important here! - } - } - } - - public static IEnumerable And(this IEnumerable enumerable, T item) - { - foreach (var x in enumerable) yield return x; - yield return item; - } - - public static IEnumerable AndIfNotNull(this IEnumerable enumerable, T item) - where T : class - { - foreach (var x in enumerable) yield return x; - if (item != null) - yield return item; - } - } -} diff --git a/src/Umbraco.ModelsBuilder/IgnoreContentTypeAttribute.cs b/src/Umbraco.ModelsBuilder/IgnoreContentTypeAttribute.cs deleted file mode 100644 index e5ab3a2e35..0000000000 --- a/src/Umbraco.ModelsBuilder/IgnoreContentTypeAttribute.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Policy; -using System.Text; -using System.Threading.Tasks; -using Umbraco.ModelsBuilder; - -namespace Umbraco.ModelsBuilder -{ - // for the time being it's all-or-nothing - // when the content type is ignored then - // - no class is generated for that content type - // - no class is generated for any child of that class - // - no interface is generated for that content type as a mixin - // - and it is ignored as a mixin ie its properties are not generated - // in the future we may way to do - // [assembly:IgnoreContentType("foo", ContentTypeIgnorable.ContentType|ContentTypeIgnorable.Mixin|ContentTypeIgnorable.MixinProperties)] - // so that we can - // - generate a class for that content type, or not - // - if not generated, generate children or not - // - if generate children, include properties or not - // - generate an interface for that content type as a mixin - // - if not generated, still generate properties in content types implementing the mixin or not - // but... I'm not even sure it makes sense - // if we don't want it... we don't want it. - - // about ignoring - // - content (don't generate the content, use as mixin) - // - mixin (don't generate the interface, use the properties) - // - mixin properties (generate the interface, not the properties) - // - mixin: local only or children too... - - /// - /// Indicates that no model should be generated for a specified content type alias. - /// - /// When a content type is ignored, its descendants are also ignored. - /// Supports trailing wildcard eg "foo*". - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] - public sealed class IgnoreContentTypeAttribute : Attribute - { - public IgnoreContentTypeAttribute(string alias /*, bool ignoreContent = true, bool ignoreMixin = true, bool ignoreMixinProperties = true*/) - {} - } -} - diff --git a/src/Umbraco.ModelsBuilder/IgnorePropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder/IgnorePropertyTypeAttribute.cs deleted file mode 100644 index 4dce0f9b7f..0000000000 --- a/src/Umbraco.ModelsBuilder/IgnorePropertyTypeAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates that no model should be generated for a specified property type alias. - /// - /// Supports trailing wildcard eg "foo*". - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - public sealed class IgnorePropertyTypeAttribute : Attribute - { - public IgnorePropertyTypeAttribute(string alias) - {} - } -} diff --git a/src/Umbraco.ModelsBuilder/ImplementContentTypeAttribute.cs b/src/Umbraco.ModelsBuilder/ImplementContentTypeAttribute.cs deleted file mode 100644 index 142f115b07..0000000000 --- a/src/Umbraco.ModelsBuilder/ImplementContentTypeAttribute.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder -{ - // NOTE - // that attribute should inherit from PublishedModelAttribute - // so we do not have different syntaxes - // but... is sealed at the moment. - - /// - /// Indicates that a (partial) class defines the model type for a specific alias. - /// - /// Though a model will be generated - so that is the way to register a rename. - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class ImplementContentTypeAttribute : Attribute - { - public ImplementContentTypeAttribute(string alias) - { } - } -} diff --git a/src/Umbraco.ModelsBuilder/ModelsBaseClassAttribute.cs b/src/Umbraco.ModelsBuilder/ModelsBaseClassAttribute.cs deleted file mode 100644 index 3c401b7fdb..0000000000 --- a/src/Umbraco.ModelsBuilder/ModelsBaseClassAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates the default base class for models. - /// - /// Otherwise it is PublishedContentModel. Would make sense to inherit from PublishedContentModel. - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] - public sealed class ModelsBaseClassAttribute : Attribute - { - public ModelsBaseClassAttribute(Type type) - {} - } -} - diff --git a/src/Umbraco.ModelsBuilder/ModelsNamespaceAttribute.cs b/src/Umbraco.ModelsBuilder/ModelsNamespaceAttribute.cs deleted file mode 100644 index 1b1d62d9bc..0000000000 --- a/src/Umbraco.ModelsBuilder/ModelsNamespaceAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates the models namespace. - /// - /// Will override anything else that might come from settings. - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] - public sealed class ModelsNamespaceAttribute : Attribute - { - public ModelsNamespaceAttribute(string modelsNamespace) - {} - } -} - diff --git a/src/Umbraco.ModelsBuilder/ModelsUsingAttribute.cs b/src/Umbraco.ModelsBuilder/ModelsUsingAttribute.cs deleted file mode 100644 index 8fe1335631..0000000000 --- a/src/Umbraco.ModelsBuilder/ModelsUsingAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Policy; -using System.Text; -using System.Threading.Tasks; -using Umbraco.ModelsBuilder; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates namespaces that should be used in generated models (in using clauses). - /// - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] - public sealed class ModelsUsingAttribute : Attribute - { - public ModelsUsingAttribute(string usingNamespace) - {} - } -} - diff --git a/src/Umbraco.ModelsBuilder/Properties/AssemblyInfo.cs b/src/Umbraco.ModelsBuilder/Properties/AssemblyInfo.cs deleted file mode 100644 index a2f8d1ae1e..0000000000 --- a/src/Umbraco.ModelsBuilder/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("Umbraco.ModelsBuilder")] -[assembly: AssemblyDescription("Umbraco ModelsBuilder")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyProduct("Umbraco CMS")] - -[assembly: ComVisible(false)] -[assembly: Guid("7020a059-c0d1-43a0-8efd-23591a0c9af6")] - -// code analysis -// IDE1006 is broken, wants _value syntax for consts, etc - and it's even confusing ppl at MS, kill it -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "~_~")] diff --git a/src/Umbraco.ModelsBuilder/PublishedPropertyTypeExtensions.cs b/src/Umbraco.ModelsBuilder/PublishedPropertyTypeExtensions.cs deleted file mode 100644 index b67ba54432..0000000000 --- a/src/Umbraco.ModelsBuilder/PublishedPropertyTypeExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Models.PublishedContent; - -namespace Umbraco.ModelsBuilder -{ - public static class PublishedPropertyTypeExtensions - { - // fixme - need to rewrite that one - we don't have prevalues anymore - //public static KeyValuePair[] PreValues(this PublishedPropertyType propertyType) - //{ - // return ApplicationContext.Current.Services.DataTypeService - // .GetPreValuesCollectionByDataTypeId(propertyType.DataType.Id) - // .PreValuesAsArray - // .Select(x => new KeyValuePair(x.Id, x.Value)) - // .ToArray(); - //} - } -} diff --git a/src/Umbraco.ModelsBuilder/PureLiveAssemblyAttribute.cs b/src/Umbraco.ModelsBuilder/PureLiveAssemblyAttribute.cs deleted file mode 100644 index dfe369dc21..0000000000 --- a/src/Umbraco.ModelsBuilder/PureLiveAssemblyAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates that an Assembly is a PureLive models assembly. - /// - /// Though technically not required, ie models will work without it, the attribute - /// can be used by Umbraco view models binder to figure out whether the model type comes - /// from a PureLive Assembly. - [Obsolete("Should use ModelsBuilderAssemblyAttribute but that requires a change in Umbraco Core.")] - [AttributeUsage(AttributeTargets.Assembly /*, AllowMultiple = false, Inherited = false*/)] - public sealed class PureLiveAssemblyAttribute : Attribute - { } -} diff --git a/src/Umbraco.ModelsBuilder/RenameContentTypeAttribute.cs b/src/Umbraco.ModelsBuilder/RenameContentTypeAttribute.cs deleted file mode 100644 index 0f985e70b3..0000000000 --- a/src/Umbraco.ModelsBuilder/RenameContentTypeAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates a model name for a specified content alias. - /// - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] - public sealed class RenameContentTypeAttribute : Attribute - { - public RenameContentTypeAttribute(string alias, string name) - {} - } -} diff --git a/src/Umbraco.ModelsBuilder/RenamePropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder/RenamePropertyTypeAttribute.cs deleted file mode 100644 index 0d8fd31b63..0000000000 --- a/src/Umbraco.ModelsBuilder/RenamePropertyTypeAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates a model name for a specified property alias. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - public sealed class RenamePropertyTypeAttribute : Attribute - { - public RenamePropertyTypeAttribute(string alias, string name) - {} - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/LiveModelsProvider.cs b/src/Umbraco.ModelsBuilder/Umbraco/LiveModelsProvider.cs deleted file mode 100644 index b6c37a3558..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/LiveModelsProvider.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Threading; -using System.Web; -using System.Web.Hosting; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.ModelsBuilder.Umbraco; -using Umbraco.Web.Cache; - -// will install only if configuration says it needs to be installed -[assembly: PreApplicationStartMethod(typeof(LiveModelsProviderModule), "Install")] - -namespace Umbraco.ModelsBuilder.Umbraco -{ - // supports LiveDll and LiveAppData - but not PureLive - public sealed class LiveModelsProvider - { - private static UmbracoServices _umbracoServices; - private static Mutex _mutex; - private static int _req; - - internal static bool IsEnabled - { - get - { - var config = UmbracoConfig.For.ModelsBuilder(); - return config.ModelsMode.IsLiveNotPure(); - // we do not manage pure live here - } - } - - internal static void Install(UmbracoServices umbracoServices) - { - // just be sure - if (!IsEnabled) - return; - - _umbracoServices = umbracoServices; - - // initialize mutex - // ApplicationId will look like "/LM/W3SVC/1/Root/AppName" - // name is system-wide and must be less than 260 chars - var name = HostingEnvironment.ApplicationID + "/UmbracoLiveModelsProvider"; - _mutex = new Mutex(false, name); - - // anything changes, and we want to re-generate models. - ContentTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; - DataTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; - - // at the end of a request since we're restarting the pool - // NOTE - this does NOT trigger - see module below - //umbracoApplication.EndRequest += GenerateModelsIfRequested; - } - - // NOTE - // Using HttpContext Items fails because CacheUpdated triggers within - // some asynchronous backend task where we seem to have no HttpContext. - - // So we use a static (non request-bound) var to register that models - // need to be generated. Could be by another request. Anyway. We could - // have collisions but... you know the risk. - - private static void RequestModelsGeneration(object sender, EventArgs args) - { - //HttpContext.Current.Items[this] = true; - Current.Logger.Debug("Requested to generate models."); - Interlocked.Exchange(ref _req, 1); - } - - public static void GenerateModelsIfRequested(object sender, EventArgs args) - { - //if (HttpContext.Current.Items[this] == null) return; - if (Interlocked.Exchange(ref _req, 0) == 0) return; - - // cannot use a simple lock here because we don't want another AppDomain - // to generate while we do... and there could be 2 AppDomains if the app restarts. - - try - { - Current.Logger.Debug("Generate models..."); - const int timeout = 2*60*1000; // 2 mins - _mutex.WaitOne(timeout); // wait until it is safe, and acquire - Current.Logger.Info("Generate models now."); - GenerateModels(); - ModelsGenerationError.Clear(); - Current.Logger.Info("Generated."); - } - catch (TimeoutException) - { - Current.Logger.Warn("Timeout, models were NOT generated."); - } - catch (Exception e) - { - ModelsGenerationError.Report("Failed to build Live models.", e); - Current.Logger.Error("Failed to generate models.", e); - } - finally - { - _mutex.ReleaseMutex(); // release - } - } - - private static void GenerateModels() - { - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; - - var bin = HostingEnvironment.MapPath("~/bin"); - if (bin == null) - throw new Exception("Panic: bin is null."); - - var config = UmbracoConfig.For.ModelsBuilder(); - - // EnableDllModels will recycle the app domain - but this request will end properly - ModelsBuilderBackOfficeController.GenerateModels(_umbracoServices, modelsDirectory, config.ModelsMode.IsAnyDll() ? bin : null); - } - } - - // have to do this because it's the only way to subscribe to EndRequest, - // module is installed by assembly attribute at the top of this file - public class LiveModelsProviderModule : IHttpModule - { - public void Init(HttpApplication app) - { - app.EndRequest += LiveModelsProvider.GenerateModelsIfRequested; - } - - public void Dispose() - { - // nothing - } - - public static void Install() - { - if (!LiveModelsProvider.IsEnabled) - return; - - HttpApplication.RegisterModule(typeof(LiveModelsProviderModule)); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderBackOfficeController.cs b/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderBackOfficeController.cs deleted file mode 100644 index 19c9bda5da..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderBackOfficeController.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Runtime.Serialization; -using System.Text; -using System.Web.Hosting; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Building; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.ModelsBuilder.Dashboard; -using Umbraco.Web.Editors; -using Umbraco.Web.WebApi.Filters; - -namespace Umbraco.ModelsBuilder.Umbraco -{ - /// - /// API controller for use in the Umbraco back office with Angular resources - /// - /// - /// We've created a different controller for the backoffice/angular specifically this is to ensure that the - /// correct CSRF security is adhered to for angular and it also ensures that this controller is not subseptipal to - /// global WebApi formatters being changed since this is always forced to only return Angular JSON Specific formats. - /// - [UmbracoApplicationAuthorize(Core.Constants.Applications.Developer)] - public class ModelsBuilderBackOfficeController : UmbracoAuthorizedJsonController - { - private readonly UmbracoServices _umbracoServices; - - public ModelsBuilderBackOfficeController(UmbracoServices umbracoServices) - { - _umbracoServices = umbracoServices; - } - - // invoked by the dashboard - // requires that the user is logged into the backoffice and has access to the developer section - // beware! the name of the method appears in modelsbuilder.controller.js - [System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers! - public HttpResponseMessage BuildModels() - { - try - { - if (!UmbracoConfig.For.ModelsBuilder().ModelsMode.SupportsExplicitGeneration()) - { - var result2 = new BuildResult { Success = false, Message = "Models generation is not enabled." }; - return Request.CreateResponse(HttpStatusCode.OK, result2, Configuration.Formatters.JsonFormatter); - } - - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; - - var bin = HostingEnvironment.MapPath("~/bin"); - if (bin == null) - throw new Exception("Panic: bin is null."); - - // EnableDllModels will recycle the app domain - but this request will end properly - GenerateModels(modelsDirectory, UmbracoConfig.For.ModelsBuilder().ModelsMode.IsAnyDll() ? bin : null); - - ModelsGenerationError.Clear(); - } - catch (Exception e) - { - ModelsGenerationError.Report("Failed to build models.", e); - } - - return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter); - } - - // invoked by the back-office - // requires that the user is logged into the backoffice and has access to the developer section - [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! - public HttpResponseMessage GetModelsOutOfDateStatus() - { - var status = OutOfDateModelsStatus.IsEnabled - ? (OutOfDateModelsStatus.IsOutOfDate - ? new OutOfDateStatus { Status = OutOfDateType.OutOfDate } - : new OutOfDateStatus { Status = OutOfDateType.Current }) - : new OutOfDateStatus { Status = OutOfDateType.Unknown }; - - return Request.CreateResponse(HttpStatusCode.OK, status, Configuration.Formatters.JsonFormatter); - } - - // invoked by the back-office - // requires that the user is logged into the backoffice and has access to the developer section - // beware! the name of the method appears in modelsbuilder.controller.js - [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! - public HttpResponseMessage GetDashboard() - { - return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter); - } - - private Dashboard GetDashboardResult() - { - return new Dashboard - { - Enable = UmbracoConfig.For.ModelsBuilder().Enable, - Text = BuilderDashboardHelper.Text(), - CanGenerate = BuilderDashboardHelper.CanGenerate(), - GenerateCausesRestart = BuilderDashboardHelper.GenerateCausesRestart(), - OutOfDateModels = BuilderDashboardHelper.AreModelsOutOfDate(), - LastError = BuilderDashboardHelper.LastError(), - }; - } - - private void GenerateModels(string modelsDirectory, string bin) - { - GenerateModels(_umbracoServices, modelsDirectory, bin); - } - - internal static void GenerateModels(UmbracoServices umbracoServices, string modelsDirectory, string bin) - { - if (!Directory.Exists(modelsDirectory)) - Directory.CreateDirectory(modelsDirectory); - - foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) - File.Delete(file); - - var typeModels = umbracoServices.GetAllTypes(); - - var ourFiles = Directory.GetFiles(modelsDirectory, "*.cs").ToDictionary(x => x, File.ReadAllText); - var parseResult = new CodeParser().ParseWithReferencedAssemblies(ourFiles); - var builder = new TextBuilder(typeModels, parseResult, UmbracoConfig.For.ModelsBuilder().ModelsNamespace); - - foreach (var typeModel in builder.GetModelsToGenerate()) - { - var sb = new StringBuilder(); - builder.Generate(sb, typeModel); - var filename = Path.Combine(modelsDirectory, typeModel.ClrName + ".generated.cs"); - File.WriteAllText(filename, sb.ToString()); - } - - // the idea was to calculate the current hash and to add it as an extra file to the compilation, - // in order to be able to detect whether a DLL is consistent with an environment - however the - // environment *might not* contain the local partial files, and thus it could be impossible to - // calculate the hash. So... maybe that's not a good idea after all? - /* - var currentHash = HashHelper.Hash(ourFiles, typeModels); - ourFiles["models.hash.cs"] = $@"using Umbraco.ModelsBuilder; -[assembly:ModelsBuilderAssembly(SourceHash = ""{currentHash}"")] -"; - */ - - if (bin != null) - { - foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) - ourFiles[file] = File.ReadAllText(file); - var compiler = new Compiler(); - compiler.Compile(builder.GetModelsNamespace(), ourFiles, bin); - } - - OutOfDateModelsStatus.Clear(); - } - - [DataContract] - internal class BuildResult - { - [DataMember(Name = "success")] - public bool Success; - [DataMember(Name = "message")] - public string Message; - } - - [DataContract] - internal class Dashboard - { - [DataMember(Name = "enable")] - public bool Enable; - [DataMember(Name = "text")] - public string Text; - [DataMember(Name = "canGenerate")] - public bool CanGenerate; - [DataMember(Name = "generateCausesRestart")] - public bool GenerateCausesRestart; - [DataMember(Name = "outOfDateModels")] - public bool OutOfDateModels; - [DataMember(Name = "lastError")] - public string LastError; - } - - internal enum OutOfDateType - { - OutOfDate, - Current, - Unknown = 100 - } - - [DataContract] - internal class OutOfDateStatus - { - [DataMember(Name = "status")] - public OutOfDateType Status { get; set; } - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComponent.cs deleted file mode 100644 index a581319ba5..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComponent.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Web; -using System.Web.Mvc; -using System.Web.Routing; -using LightInject; -using Umbraco.Core; -using Umbraco.Core.Components; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; -using Umbraco.Core.Services.Implement; -using Umbraco.ModelsBuilder.Api; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.Web; -using Umbraco.Web.PublishedCache.NuCache; -using Umbraco.Web.UI.JavaScript; - -namespace Umbraco.ModelsBuilder.Umbraco -{ - [RequiredComponent(typeof(NuCacheComponent))] - [RuntimeLevel(MinLevel = RuntimeLevel.Run)] - public class ModelsBuilderComponent : UmbracoComponentBase, IUmbracoCoreComponent - { - public override void Compose(Composition composition) - { - base.Compose(composition); - - composition.Container.Register(new PerContainerLifetime()); - - var config = UmbracoConfig.For.ModelsBuilder(); - - if (config.ModelsMode == ModelsMode.PureLive) - ComposeForLiveModels(composition.Container); - else if (config.EnableFactory) - ComposeForDefaultModelsFactory(composition.Container); - - // always setup the dashboard - InstallServerVars(composition.Container.GetInstance().Level); - composition.Container.Register(typeof(ModelsBuilderBackOfficeController), new PerRequestLifeTime()); - - // setup the API if enabled (and in debug mode) - if (config.ApiServer) - composition.Container.Register(typeof(ModelsBuilderApiController), new PerRequestLifeTime()); - } - - public void Initialize(UmbracoServices umbracoServices) - { - var config = UmbracoConfig.For.ModelsBuilder(); - - if (config.Enable) - FileService.SavingTemplate += FileService_SavingTemplate; - - // fixme LiveModelsProvider should not be static - if (config.ModelsMode.IsLiveNotPure()) - LiveModelsProvider.Install(umbracoServices); - - // fixme OutOfDateModelsStatus should not be static - if (config.FlagOutOfDateModels) - OutOfDateModelsStatus.Install(); - } - - private void ComposeForDefaultModelsFactory(IServiceContainer container) - { - container.RegisterSingleton(factory - => new PublishedModelFactory(factory.GetInstance().GetTypes())); - } - - private void ComposeForLiveModels(IServiceContainer container) - { - container.RegisterSingleton(); - - // the following would add @using statement in every view so user's don't - // have to do it - however, then noone understands where the @using statement - // comes from, and it cannot be avoided / removed --- DISABLED - // - /* - // no need for @using in views - // note: - // we are NOT using the in-code attribute here, config is required - // because that would require parsing the code... and what if it changes? - // we can AddGlobalImport not sure we can remove one anyways - var modelsNamespace = Configuration.Config.ModelsNamespace; - if (string.IsNullOrWhiteSpace(modelsNamespace)) - modelsNamespace = Configuration.Config.DefaultModelsNamespace; - System.Web.WebPages.Razor.WebPageRazorHost.AddGlobalImport(modelsNamespace); - */ - } - - private void InstallServerVars(RuntimeLevel level) - { - // register our url - for the backoffice api - ServerVariablesParser.Parsing += (sender, serverVars) => - { - if (!serverVars.ContainsKey("umbracoUrls")) - throw new Exception("Missing umbracoUrls."); - var umbracoUrlsObject = serverVars["umbracoUrls"]; - if (umbracoUrlsObject == null) - throw new Exception("Null umbracoUrls"); - if (!(umbracoUrlsObject is Dictionary umbracoUrls)) - throw new Exception("Invalid umbracoUrls"); - - if (!serverVars.ContainsKey("umbracoPlugins")) - throw new Exception("Missing umbracoPlugins."); - if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) - throw new Exception("Invalid umbracoPlugins"); - - if (HttpContext.Current == null) throw new InvalidOperationException("HttpContext is null"); - var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData())); - - umbracoUrls["modelsBuilderBaseUrl"] = urlHelper.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); - umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(level); - }; - } - - private Dictionary GetModelsBuilderSettings(RuntimeLevel level) - { - if (level != RuntimeLevel.Run) - return null; - - var settings = new Dictionary - { - {"enabled", UmbracoConfig.For.ModelsBuilder().Enable} - }; - - return settings; - } - - /// - /// Used to check if a template is being created based on a document type, in this case we need to - /// ensure the template markup is correct based on the model name of the document type - /// - /// - /// - private void FileService_SavingTemplate(IFileService sender, Core.Events.SaveEventArgs e) - { - // don't do anything if the factory is not enabled - // because, no factory = no models (even if generation is enabled) - if (!UmbracoConfig.For.ModelsBuilder().EnableFactory) return; - - // don't do anything if this special key is not found - if (!e.AdditionalData.ContainsKey("CreateTemplateForContentType")) return; - - // ensure we have the content type alias - if (!e.AdditionalData.ContainsKey("ContentTypeAlias")) - throw new InvalidOperationException("The additionalData key: ContentTypeAlias was not found"); - - foreach (var template in e.SavedEntities) - { - // if it is in fact a new entity (not been saved yet) and the "CreateTemplateForContentType" key - // is found, then it means a new template is being created based on the creation of a document type - if (!template.HasIdentity && string.IsNullOrWhiteSpace(template.Content)) - { - // ensure is safe and always pascal cased, per razor standard - // + this is how we get the default model name in Umbraco.ModelsBuilder.Umbraco.Application - var alias = e.AdditionalData["ContentTypeAlias"].ToString(); - var name = template.Name; // will be the name of the content type since we are creating - var className = UmbracoServices.GetClrName(name, alias); - - var modelNamespace = UmbracoConfig.For.ModelsBuilder().ModelsNamespace; - - // we do not support configuring this at the moment, so just let Umbraco use its default value - //var modelNamespaceAlias = ...; - - var markup = ViewHelper.GetDefaultFileContent( - modelClassName: className, - modelNamespace: modelNamespace/*, - modelNamespaceAlias: modelNamespaceAlias*/); - - //set the template content to the new markup - template.Content = markup; - } - } - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs b/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs deleted file mode 100644 index 20f5e94b64..0000000000 --- a/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.Web.Editors; -using Umbraco.Web.Models.ContentEditing; - -namespace Umbraco.ModelsBuilder.Validation -{ - /// - /// Used to validate the aliases for the content type when MB is enabled to ensure that - /// no illegal aliases are used - /// - internal class ContentTypeModelValidator : ContentTypeModelValidatorBase - { - } - - /// - /// Used to validate the aliases for the content type when MB is enabled to ensure that - /// no illegal aliases are used - /// - internal class MediaTypeModelValidator : ContentTypeModelValidatorBase - { - } - - /// - /// Used to validate the aliases for the content type when MB is enabled to ensure that - /// no illegal aliases are used - /// - internal class MemberTypeModelValidator : ContentTypeModelValidatorBase - { - } - - internal abstract class ContentTypeModelValidatorBase : EditorValidator - where TModel: ContentTypeSave - where TProperty: PropertyTypeBasic - { - protected override IEnumerable Validate(TModel model) - { - //don't do anything if we're not enabled - if (UmbracoConfig.For.ModelsBuilder().Enable) - { - var properties = model.Groups.SelectMany(x => x.Properties) - .Where(x => x.Inherited == false) - .ToArray(); - - foreach (var prop in properties) - { - var propertyGroup = model.Groups.Single(x => x.Properties.Contains(prop)); - - if (model.Alias.ToLowerInvariant() == prop.Alias.ToLowerInvariant()) - yield return new ValidationResult(string.Format("With Models Builder enabled, you can't have a property with a the alias \"{0}\" when the content type alias is also \"{0}\".", prop.Alias), new[] - { - string.Format("Groups[{0}].Properties[{1}].Alias", model.Groups.IndexOf(propertyGroup), propertyGroup.Properties.IndexOf(prop)) - }); - - //we need to return the field name with an index so it's wired up correctly - var groupIndex = model.Groups.IndexOf(propertyGroup); - var propertyIndex = propertyGroup.Properties.IndexOf(prop); - - var validationResult = ValidateProperty(prop, groupIndex, propertyIndex); - if (validationResult != null) - { - yield return validationResult; - } - } - } - } - - private ValidationResult ValidateProperty(PropertyTypeBasic property, int groupIndex, int propertyIndex) - { - //don't let them match any properties or methods in IPublishedContent - //TODO: There are probably more! - var reservedProperties = typeof(IPublishedContent).GetProperties().Select(x => x.Name).ToArray(); - var reservedMethods = typeof(IPublishedContent).GetMethods().Select(x => x.Name).ToArray(); - - var alias = property.Alias; - - if (reservedProperties.InvariantContains(alias) || reservedMethods.InvariantContains(alias)) - { - return new ValidationResult( - string.Format("The alias {0} is a reserved term and cannot be used", alias), new[] - { - string.Format("Groups[{0}].Properties[{1}].Alias", groupIndex, propertyIndex) - }); - } - - return null; - } - } -} diff --git a/src/Umbraco.TestData/SegmentTestController.cs b/src/Umbraco.TestData/SegmentTestController.cs new file mode 100644 index 0000000000..bcfdfc4ac4 --- /dev/null +++ b/src/Umbraco.TestData/SegmentTestController.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Mvc; + +namespace Umbraco.TestData +{ + public class SegmentTestController : SurfaceController + { + + public ActionResult EnableDocTypeSegments(string alias, string propertyTypeAlias) + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return HttpNotFound(); + + var ct = Services.ContentTypeService.Get(alias); + if (ct == null) + return Content($"No document type found by alias {alias}"); + + var propType = ct.PropertyTypes.FirstOrDefault(x => x.Alias == propertyTypeAlias); + if (propType == null) + return Content($"The document type {alias} does not have a property type {propertyTypeAlias ?? "null"}"); + + if (ct.Variations.VariesBySegment()) + return Content($"The document type {alias} already allows segments, nothing has been changed"); + + ct.Variations = ct.Variations.SetFlag(ContentVariation.Segment); + + propType.Variations = propType.Variations.SetFlag(ContentVariation.Segment); + + Services.ContentTypeService.Save(ct); + return Content($"The document type {alias} and property type {propertyTypeAlias} now allows segments"); + } + + public ActionResult DisableDocTypeSegments(string alias) + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return HttpNotFound(); + + var ct = Services.ContentTypeService.Get(alias); + if (ct == null) + return Content($"No document type found by alias {alias}"); + + if (!ct.VariesBySegment()) + return Content($"The document type {alias} does not allow segments, nothing has been changed"); + + ct.Variations = ct.Variations.UnsetFlag(ContentVariation.Segment); + + Services.ContentTypeService.Save(ct); + return Content($"The document type {alias} no longer allows segments"); + } + + public ActionResult AddSegmentData(int contentId, string propertyAlias, string value, string segment, string culture = null) + { + var content = Services.ContentService.GetById(contentId); + if (content == null) + return Content($"No content found by id {contentId}"); + + if (!content.HasProperty(propertyAlias)) + return Content($"The content by id {contentId} does not contain a property with alias {propertyAlias}"); + + if (content.ContentType.VariesByCulture() && culture.IsNullOrWhiteSpace()) + return Content($"The content by id {contentId} varies by culture but no culture was specified"); + + content.SetValue(propertyAlias, value, culture, segment); + Services.ContentService.Save(content); + + return Content($"Segment value has been set on content {contentId} for property {propertyAlias}"); + } + } +} diff --git a/src/Umbraco.TestData/Umbraco.TestData.csproj b/src/Umbraco.TestData/Umbraco.TestData.csproj index b0d6b5677b..d61321ebb8 100644 --- a/src/Umbraco.TestData/Umbraco.TestData.csproj +++ b/src/Umbraco.TestData/Umbraco.TestData.csproj @@ -41,6 +41,7 @@
+ diff --git a/src/Umbraco.Tests/ModelsBuilder/BuilderTests.cs b/src/Umbraco.Tests/ModelsBuilder/BuilderTests.cs new file mode 100644 index 0000000000..e1c3ecc891 --- /dev/null +++ b/src/Umbraco.Tests/ModelsBuilder/BuilderTests.cs @@ -0,0 +1,429 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.ModelsBuilder.Embedded; +using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.ModelsBuilder.Embedded.Configuration; + +namespace Umbraco.Tests.ModelsBuilder +{ + [TestFixture] + public class BuilderTests + { + + [Test] + public void GenerateSimpleType() + { + // Umbraco returns nice, pascal-cased names + + var type1 = new TypeModel + { + Id = 1, + Alias = "type1", + ClrName = "Type1", + ParentId = 0, + BaseType = null, + ItemType = TypeModel.ItemTypes.Content, + }; + type1.Properties.Add(new PropertyModel + { + Alias = "prop1", + ClrName = "Prop1", + ModelClrType = typeof(string), + }); + + var types = new[] { type1 }; + + var code = new Dictionary + { + }; + + var builder = new TextBuilder(Mock.Of(), types); + var btypes = builder.TypeModels; + + var sb = new StringBuilder(); + builder.Generate(sb, builder.GetModelsToGenerate().First()); + var gen = sb.ToString(); + + var version = ApiVersion.Current.Version; + var expected = @"//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Umbraco.ModelsBuilder v" + version + @" +// +// Changes to this file will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Web; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web; +using Umbraco.ModelsBuilder.Embedded; + +namespace Umbraco.Web.PublishedModels +{ + [PublishedModel(""type1"")] + public partial class Type1 : PublishedContentModel + { + // helpers +#pragma warning disable 0109 // new is redundant + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")] + public new const string ModelTypeAlias = ""type1""; + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")] + public new const PublishedItemType ModelItemType = PublishedItemType.Content; + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")] + public new static IPublishedContentType GetModelContentType() + => PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias); + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")] + public static IPublishedPropertyType GetModelPropertyType(Expression> selector) + => PublishedModelUtility.GetModelPropertyType(GetModelContentType(), selector); +#pragma warning restore 0109 + + // ctor + public Type1(IPublishedContent content) + : base(content) + { } + + // properties + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")] + [ImplementPropertyType(""prop1"")] + public string Prop1 => this.Value(""prop1""); + } +} +"; + Console.WriteLine(gen); + Assert.AreEqual(expected.ClearLf(), gen); + } + + [Test] + public void GenerateSimpleType_Ambiguous_Issue() + { + // Umbraco returns nice, pascal-cased names + + var type1 = new TypeModel + { + Id = 1, + Alias = "type1", + ClrName = "Type1", + ParentId = 0, + BaseType = null, + ItemType = TypeModel.ItemTypes.Content, + }; + type1.Properties.Add(new PropertyModel + { + Alias = "foo", + ClrName = "Foo", + ModelClrType = typeof(IEnumerable<>).MakeGenericType(ModelType.For("foo")), + }); + + var type2 = new TypeModel + { + Id = 2, + Alias = "foo", + ClrName = "Foo", + ParentId = 0, + BaseType = null, + ItemType = TypeModel.ItemTypes.Element, + }; + + var types = new[] { type1, type2 }; + + var code = new Dictionary + { + { "code", @" +namespace Umbraco.Web.PublishedModels +{ + public partial class Foo + { + } +} +" } + }; + + var builder = new TextBuilder(Mock.Of(), types); + var btypes = builder.TypeModels; + + builder.ModelsNamespace = "Umbraco.Web.PublishedModels"; + + var sb1 = new StringBuilder(); + builder.Generate(sb1, builder.GetModelsToGenerate().Skip(1).First()); + var gen1 = sb1.ToString(); + Console.WriteLine(gen1); + + var sb = new StringBuilder(); + builder.Generate(sb, builder.GetModelsToGenerate().First()); + var gen = sb.ToString(); + + var version = ApiVersion.Current.Version; + var expected = @"//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Umbraco.ModelsBuilder v" + version + @" +// +// Changes to this file will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Web; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web; +using Umbraco.ModelsBuilder.Embedded; + +namespace Umbraco.Web.PublishedModels +{ + [PublishedModel(""type1"")] + public partial class Type1 : PublishedContentModel + { + // helpers +#pragma warning disable 0109 // new is redundant + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")] + public new const string ModelTypeAlias = ""type1""; + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")] + public new const PublishedItemType ModelItemType = PublishedItemType.Content; + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")] + public new static IPublishedContentType GetModelContentType() + => PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias); + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")] + public static IPublishedPropertyType GetModelPropertyType(Expression> selector) + => PublishedModelUtility.GetModelPropertyType(GetModelContentType(), selector); +#pragma warning restore 0109 + + // ctor + public Type1(IPublishedContent content) + : base(content) + { } + + // properties + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder"", """ + version + @""")] + [ImplementPropertyType(""foo"")] + public global::System.Collections.Generic.IEnumerable Foo => this.Value>(""foo""); + } +} +"; + Console.WriteLine(gen); + Assert.AreEqual(expected.ClearLf(), gen); + } + + [Test] + public void GenerateAmbiguous() + { + // NOTE: since + + var type1 = new TypeModel + { + Id = 1, + Alias = "type1", + ClrName = "Type1", + ParentId = 0, + BaseType = null, + ItemType = TypeModel.ItemTypes.Content, + IsMixin = true, + }; + type1.Properties.Add(new PropertyModel + { + Alias = "prop1", + ClrName = "Prop1", + ModelClrType = typeof(IPublishedContent), + }); + type1.Properties.Add(new PropertyModel + { + Alias = "prop2", + ClrName = "Prop2", + ModelClrType = typeof(global::System.Text.StringBuilder), + }); + type1.Properties.Add(new PropertyModel + { + Alias = "prop3", + ClrName = "Prop3", + ModelClrType = typeof(global::Umbraco.Core.IO.FileSecurityException), + }); + var types = new[] { type1 }; + + var code = new Dictionary + { + }; + + var builder = new TextBuilder(Mock.Of(), types); + builder.ModelsNamespace = "Umbraco.ModelsBuilder.Models"; // forces conflict with Umbraco.ModelsBuilder.Umbraco + var btypes = builder.TypeModels; + + var sb = new StringBuilder(); + foreach (var model in builder.GetModelsToGenerate()) + builder.Generate(sb, model); + var gen = sb.ToString(); + + Console.WriteLine(gen); + + Assert.IsTrue(gen.Contains(" global::Umbraco.Core.Models.PublishedContent.IPublishedContent Prop1")); + Assert.IsTrue(gen.Contains(" global::System.Text.StringBuilder Prop2")); + Assert.IsTrue(gen.Contains(" global::Umbraco.Core.IO.FileSecurityException Prop3")); + } + + [TestCase("int", typeof(int))] + [TestCase("global::System.Collections.Generic.IEnumerable", typeof(IEnumerable))] + [TestCase("global::Umbraco.Tests.ModelsBuilder.BuilderTestsClass1", typeof(BuilderTestsClass1))] + [TestCase("global::Umbraco.Tests.ModelsBuilder.BuilderTests.Class1", typeof(Class1))] + public void WriteClrType(string expected, Type input) + { + // note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true + // which means global:: syntax will be applied to most things + + var builder = new TextBuilder(); + builder.ModelsNamespaceForTests = "ModelsNamespace"; + var sb = new StringBuilder(); + builder.WriteClrType(sb, input); + Assert.AreEqual(expected, sb.ToString()); + } + + [TestCase("int", typeof(int))] + [TestCase("global::System.Collections.Generic.IEnumerable", typeof(IEnumerable))] + [TestCase("global::Umbraco.Tests.ModelsBuilder.BuilderTestsClass1", typeof(BuilderTestsClass1))] + [TestCase("global::Umbraco.Tests.ModelsBuilder.BuilderTests.Class1", typeof(Class1))] + public void WriteClrTypeUsing(string expected, Type input) + { + // note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true + // which means global:: syntax will be applied to most things + + var builder = new TextBuilder(); + builder.Using.Add("Umbraco.Tests.ModelsBuilder"); + builder.ModelsNamespaceForTests = "ModelsNamespace"; + var sb = new StringBuilder(); + builder.WriteClrType(sb, input); + Assert.AreEqual(expected, sb.ToString()); + } + + [Test] + public void WriteClrType_WithUsing() + { + var builder = new TextBuilder(); + builder.Using.Add("System.Text"); + builder.ModelsNamespaceForTests = "Umbraco.Tests.ModelsBuilder.Models"; + var sb = new StringBuilder(); + builder.WriteClrType(sb, typeof(StringBuilder)); + + // note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true + // which means global:: syntax will be applied to most things + + Assert.AreEqual("global::System.Text.StringBuilder", sb.ToString()); + } + + [Test] + public void WriteClrTypeAnother_WithoutUsing() + { + var builder = new TextBuilder(); + builder.ModelsNamespaceForTests = "Umbraco.Tests.ModelsBuilder.Models"; + var sb = new StringBuilder(); + builder.WriteClrType(sb, typeof(StringBuilder)); + Assert.AreEqual("global::System.Text.StringBuilder", sb.ToString()); + } + + [Test] + public void WriteClrType_Ambiguous1() + { + var builder = new TextBuilder(); + builder.Using.Add("System.Text"); + builder.Using.Add("Umbraco.Tests.ModelsBuilder"); + builder.ModelsNamespaceForTests = "SomeRandomNamespace"; + var sb = new StringBuilder(); + builder.WriteClrType(sb, typeof(global::System.Text.ASCIIEncoding)); + + // note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true + // which means global:: syntax will be applied to most things + + Assert.AreEqual("global::System.Text.ASCIIEncoding", sb.ToString()); + } + + [Test] + public void WriteClrType_Ambiguous() + { + var builder = new TextBuilder(); + builder.Using.Add("System.Text"); + builder.Using.Add("Umbraco.Tests.ModelsBuilder"); + builder.ModelsNamespaceForTests = "SomeBorkedNamespace"; + var sb = new StringBuilder(); + builder.WriteClrType(sb, typeof(global::System.Text.ASCIIEncoding)); + + // note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true + // which means global:: syntax will be applied to most things + + Assert.AreEqual("global::System.Text.ASCIIEncoding", sb.ToString()); + } + + [Test] + public void WriteClrType_Ambiguous2() + { + var builder = new TextBuilder(); + builder.Using.Add("System.Text"); + builder.Using.Add("Umbraco.Tests.ModelsBuilder"); + builder.ModelsNamespaceForTests = "SomeRandomNamespace"; + var sb = new StringBuilder(); + builder.WriteClrType(sb, typeof(ASCIIEncoding)); + + // note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true + // which means global:: syntax will be applied to most things + + Assert.AreEqual("global::Umbraco.Tests.ModelsBuilder.ASCIIEncoding", sb.ToString()); + } + + [Test] + public void WriteClrType_AmbiguousNot() + { + var builder = new TextBuilder(); + builder.Using.Add("System.Text"); + builder.Using.Add("Umbraco.Tests.ModelsBuilder"); + builder.ModelsNamespaceForTests = "Umbraco.Tests.ModelsBuilder.Models"; + var sb = new StringBuilder(); + builder.WriteClrType(sb, typeof(ASCIIEncoding)); + + // note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true + // which means global:: syntax will be applied to most things + + Assert.AreEqual("global::Umbraco.Tests.ModelsBuilder.ASCIIEncoding", sb.ToString()); + } + + [Test] + public void WriteClrType_AmbiguousWithNested() + { + var builder = new TextBuilder(); + builder.Using.Add("System.Text"); + builder.Using.Add("Umbraco.Tests.ModelsBuilder"); + builder.ModelsNamespaceForTests = "SomeRandomNamespace"; + var sb = new StringBuilder(); + builder.WriteClrType(sb, typeof(ASCIIEncoding.Nested)); + + // note - these assertions differ from the original tests in MB because in the embedded version, the result of Builder.IsAmbiguousSymbol is always true + // which means global:: syntax will be applied to most things + + Assert.AreEqual("global::Umbraco.Tests.ModelsBuilder.ASCIIEncoding.Nested", sb.ToString()); + } + + public class Class1 { } + } + +// make it public to be ambiguous (see above) + public class ASCIIEncoding + { + // can we handle nested types? + public class Nested { } + } + + class BuilderTestsClass1 {} + + public class System { } +} diff --git a/src/Umbraco.Tests/ModelsBuilder/ConfigTests.cs b/src/Umbraco.Tests/ModelsBuilder/ConfigTests.cs new file mode 100644 index 0000000000..5e122ad0fa --- /dev/null +++ b/src/Umbraco.Tests/ModelsBuilder/ConfigTests.cs @@ -0,0 +1,49 @@ +using System.Configuration; +using NUnit.Framework; +using Umbraco.ModelsBuilder.Embedded.Configuration; + +namespace Umbraco.Tests.ModelsBuilder +{ + [TestFixture] + public class ModelsBuilderConfigTests + { + [Test] + public void Test1() + { + var config = new ModelsBuilderConfig(modelsNamespace: "test1"); + Assert.AreEqual("test1", config.ModelsNamespace); + } + + [Test] + public void Test2() + { + var config = new ModelsBuilderConfig(modelsNamespace: "test2"); + Assert.AreEqual("test2", config.ModelsNamespace); + } + + [Test] + public void DefaultModelsNamespace() + { + var config = new ModelsBuilderConfig(); + Assert.AreEqual(ModelsBuilderConfig.DefaultModelsNamespace, config.ModelsNamespace); + } + + [TestCase("c:/path/to/root", "~/dir/models", false, "c:\\path\\to\\root\\dir\\models")] + [TestCase("c:/path/to/root", "~/../../dir/models", true, "c:\\path\\dir\\models")] + [TestCase("c:/path/to/root", "c:/another/path/to/elsewhere", true, "c:\\another\\path\\to\\elsewhere")] + public void GetModelsDirectoryTests(string root, string config, bool acceptUnsafe, string expected) + { + Assert.AreEqual(expected, ModelsBuilderConfig.GetModelsDirectory(root, config, acceptUnsafe)); + } + + [TestCase("c:/path/to/root", "~/../../dir/models", false)] + [TestCase("c:/path/to/root", "c:/another/path/to/elsewhere", false)] + public void GetModelsDirectoryThrowsTests(string root, string config, bool acceptUnsafe) + { + Assert.Throws(() => + { + var modelsDirectory = ModelsBuilderConfig.GetModelsDirectory(root, config, acceptUnsafe); + }); + } + } +} diff --git a/src/Umbraco.Tests/ModelsBuilder/StringExtensions.cs b/src/Umbraco.Tests/ModelsBuilder/StringExtensions.cs new file mode 100644 index 0000000000..361d104911 --- /dev/null +++ b/src/Umbraco.Tests/ModelsBuilder/StringExtensions.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Tests.ModelsBuilder +{ + public static class StringExtensions + { + public static string ClearLf(this string s) + { + return s.Replace("\r", ""); + } + } +} diff --git a/src/Umbraco.Tests/ModelsBuilder/UmbracoApplicationTests.cs b/src/Umbraco.Tests/ModelsBuilder/UmbracoApplicationTests.cs new file mode 100644 index 0000000000..4d2ae0e6c6 --- /dev/null +++ b/src/Umbraco.Tests/ModelsBuilder/UmbracoApplicationTests.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using Umbraco.ModelsBuilder.Embedded; +using Umbraco.ModelsBuilder.Embedded.Building; + +namespace Umbraco.Tests.ModelsBuilder +{ + [TestFixture] + public class UmbracoApplicationTests + { + //[Test] + //public void Test() + //{ + // // start and terminate + // using (var app = Application.GetApplication(TestOptions.ConnectionString, TestOptions.DatabaseProvider)) + // { } + + // // start and terminate + // using (var app = Application.GetApplication(TestOptions.ConnectionString, TestOptions.DatabaseProvider)) + // { } + + // // start, use and terminate + // using (var app = Application.GetApplication(TestOptions.ConnectionString, TestOptions.DatabaseProvider)) + // { + // var types = app.GetContentTypes(); + // } + //} + + [Test] + public void ThrowsOnDuplicateAliases() + { + var typeModels = new List + { + new TypeModel { ItemType = TypeModel.ItemTypes.Content, Alias = "content1" }, + new TypeModel { ItemType = TypeModel.ItemTypes.Content, Alias = "content2" }, + new TypeModel { ItemType = TypeModel.ItemTypes.Media, Alias = "media1" }, + new TypeModel { ItemType = TypeModel.ItemTypes.Media, Alias = "media2" }, + new TypeModel { ItemType = TypeModel.ItemTypes.Member, Alias = "member1" }, + new TypeModel { ItemType = TypeModel.ItemTypes.Member, Alias = "member2" }, + }; + + Assert.AreEqual(6, UmbracoServices.EnsureDistinctAliases(typeModels).Count); + + typeModels.Add(new TypeModel { ItemType = TypeModel.ItemTypes.Media, Alias = "content1" }); + + try + { + UmbracoServices.EnsureDistinctAliases(typeModels); + } + catch (NotSupportedException e) + { + Console.WriteLine(e.Message); + return; + } + + Assert.Fail("Expected NotSupportedException."); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 6d12ef6268..ff923bb04b 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -79,7 +79,7 @@ - + 1.8.14 @@ -134,6 +134,10 @@ + + + + @@ -553,6 +557,10 @@ {31785BC3-256C-4613-B2F5-A1B0BDDED8C1} Umbraco.Core + + {52ac0ba8-a60e-4e36-897b-e8b97a54ed1c} + Umbraco.ModelsBuilder.Embedded + {651E1350-91B6-44B7-BD60-7207006D7003} Umbraco.Web diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/code.less b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/code.less index 0b90a13059..5eb8b638a2 100644 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/code.less +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/code.less @@ -9,14 +9,13 @@ pre.code { padding: 0 3px 2px; #font > #family > .monospace; font-size: @baseFontSize - 2; - color: @grayDark; + color: @blueExtraDark; .border-radius(3px); } // Inline code code { padding: 2px 4px; - color: #d14; background-color: #f7f7f9; border: 1px solid #e1e1e8; white-space: nowrap; diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less index 94f229a191..5d0e1c8e7e 100644 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less @@ -79,11 +79,9 @@ // Hover/Focus state // ----------- .dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus, .dropdown-menu > li > button:hover, -.dropdown-menu > li > button:focus, .dropdown-submenu:hover > a, -.dropdown-submenu:focus > a { +.dropdown-submenu:hover > button { text-decoration: none; color: @dropdownLinkColorHover; #gradient > .vertical(@dropdownLinkBackgroundHover, darken(@dropdownLinkBackgroundHover, 5%)); @@ -92,8 +90,7 @@ // Active state // ------------ .dropdown-menu > .active > a, -.dropdown-menu > .active > a:hover, -.dropdown-menu > .active > a:focus { +.dropdown-menu > .active > a:hover { color: @dropdownLinkColorActive; text-decoration: none; outline: 0; @@ -104,13 +101,11 @@ // -------------- // Gray out text and ensure the hover/focus state remains gray .dropdown-menu > .disabled > a, -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { +.dropdown-menu > .disabled > a:hover { color: @grayLight; } // Nuke hover/focus effects -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { +.dropdown-menu > .disabled > a:hover { text-decoration: none; background-color: transparent; background-image: none; // Remove CSS gradient diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 2b92302684..0f02aba5e2 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -38,7 +38,7 @@ "lazyload-js": "1.0.0", "moment": "2.22.2", "ng-file-upload": "12.2.13", - "nouislider": "14.0.2", + "nouislider": "14.1.1", "npm": "6.12.0", "signalr": "2.4.0", "spectrum-colorpicker": "1.8.0", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js index 19ebe448e0..541cc647fb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js @@ -76,7 +76,6 @@ // Check if it is a new user const inviteVal = $location.search().invite; - vm.baseTitle = $scope.$root.locationTitle; //1 = enter password, 2 = password set, 3 = invalid token if (inviteVal && (inviteVal === "1" || inviteVal === "2")) { @@ -457,9 +456,7 @@ break; } - if (title != null) { - $scope.$root.locationTitle = title + " - " + vm.baseTitle; - } + $scope.$emit("$changeTitle", title); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js index 5d34ad2906..79cb99cf07 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js @@ -75,6 +75,8 @@ scope.displayLabelOff = ""; function onInit() { + scope.inputId = scope.inputId || "umb-toggle_" + String.CreateGuid(); + setLabelText(); // must wait until the current digest cycle is finished before we emit this event on init, // otherwise other property editors might not yet be ready to receive the event diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js index 6559e16206..431a05778c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js @@ -224,7 +224,8 @@ Use this directive to construct a header inside the main editor window. if (editorState.current) { //to do make work for user create/edit // to do make it work for user group create/ edit - // to make it work for language edit/create + // to do make it work for language edit/create + // to do make it work for log viewer scope.isNew = editorState.current.id === 0 || editorState.current.id === "0" || editorState.current.id === -1 || diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautoresize.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautoresize.directive.js index 56dfb6b180..69ec1be805 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautoresize.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautoresize.directive.js @@ -138,7 +138,11 @@ angular.module("umbraco.directives") var unbindModelWatcher = scope.$watch(function() { return ngModelController.$modelValue; }, function(newValue) { - update(true); + $timeout( + function() { + update(true); + } + ); }); scope.$on('$destroy', function() { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js index eb3c134f0b..d562b21d52 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js @@ -46,6 +46,8 @@ vm.change = change; function onInit() { + vm.inputId = vm.inputId || "umb-check_" + String.CreateGuid(); + // If a labelKey is passed let's update the returned text if it's does not contain an opening square bracket [ if (vm.labelKey) { localizationService.localize(vm.labelKey).then(function (data) { @@ -80,7 +82,8 @@ disabled: "<", required: "<", onChange: "&?", - cssClass: "@?" + cssClass: "@?", + iconClass: "@?" } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js index f3ecac2a74..7ed84547f1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js @@ -44,6 +44,8 @@ vm.change = change; function onInit() { + vm.inputId = vm.inputId || "umb-radio_" + String.CreateGuid(); + // If a labelKey is passed let's update the returned text if it's does not contain an opening square bracket [ if (vm.labelKey) { localizationService.localize(vm.labelKey).then(function (data) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/member/umbmembernodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/member/umbmembernodeinfo.directive.js new file mode 100644 index 0000000000..3b6a2c069a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/member/umbmembernodeinfo.directive.js @@ -0,0 +1,73 @@ +(function () { + 'use strict'; + + function MemberNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService) { + + function link(scope, element, attrs, ctrl) { + + var evts = []; + + //TODO: Infinite editing is not working yet. + scope.allowChangeMemberType = false; + + function onInit() { + // make sure dates are formatted to the user's locale + formatDatesToLocal(); + } + + function formatDatesToLocal() { + // get current backoffice user and format dates + userService.getCurrentUser().then(function (currentUser) { + scope.node.createDateFormatted = dateHelper.getLocalDate(scope.node.createDate, currentUser.locale, 'LLL'); + scope.node.updateDateFormatted = dateHelper.getLocalDate(scope.node.updateDate, currentUser.locale, 'LLL'); + }); + } + + scope.openMemberType = function (memberType) { + var editor = { + id: memberType.id, + submit: function (model) { + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.memberTypeEditor(editor); + }; + + // watch for content updates - reload content when node is saved, published etc. + scope.$watch('node.updateDate', function (newValue, oldValue) { + if (!newValue) { return; } + if (newValue === oldValue) { return; } + + // Update the create and update dates + formatDatesToLocal(); + }); + + //ensure to unregister from all events! + scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); + + onInit(); + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/member/umb-member-node-info.html', + scope: { + node: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbMemberNodeInfo', MemberNodeInfoDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index 31e797c6b4..9c33b35e82 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -16,9 +16,6 @@ angular.module("umbraco.directives") replace: true, templateUrl: 'views/components/property/umb-property.html', link: function (scope) { - - scope.propertyActions = []; - userService.getCurrentUser().then(function (u) { var isAdmin = u.userGroups.indexOf('admin') !== -1; scope.propertyAlias = (Umbraco.Sys.ServerVariables.isDebuggingEnabled === true || isAdmin) ? scope.property.alias : null; @@ -36,6 +33,7 @@ angular.module("umbraco.directives") $scope.property.propertyErrorMessage = errorMsg; }; + $scope.propertyActions = []; self.setPropertyActions = function(actions) { $scope.propertyActions = actions; }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyeditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyeditor.directive.js index 32cbbb31ec..5eac7e5e24 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyeditor.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyeditor.directive.js @@ -15,7 +15,7 @@ function umbPropEditor(umbPropEditorHelper) { preview: "<" }, - require: "^^form", + require: ["^^form", "?^umbProperty"], restrict: 'E', replace: true, templateUrl: 'views/components/property/umb-property-editor.html', @@ -24,7 +24,10 @@ function umbPropEditor(umbPropEditorHelper) { //we need to copy the form controller val to our isolated scope so that //it get's carried down to the child scopes of this! //we'll also maintain the current form name. - scope[ctrl.$name] = ctrl; + scope[ctrl[0].$name] = ctrl[0]; + + // We will capture a reference to umbProperty in this Directive and pass it on to the Scope, so Property-Editor controllers can use it. + scope["umbProperty"] = ctrl[1]; if(!scope.model.alias){ scope.model.alias = Math.random().toString(36).slice(2); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcheckmark.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcheckmark.directive.js index b0899f0f8b..d1fefeae5a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcheckmark.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcheckmark.directive.js @@ -10,7 +10,8 @@ templateUrl: 'views/components/umb-checkmark.html', scope: { size: "@?", - checked: "=" + checked: "=", + readonly: "@?" } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcodesnippet.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcodesnippet.directive.js new file mode 100644 index 0000000000..f0dad31ee2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcodesnippet.directive.js @@ -0,0 +1,119 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbCodeSnippet +@restrict E +@scope + +@description + +

Markup example

+
+	
+ + + {{code}} + + +
+
+ +

Controller example

+
+	(function () {
+		"use strict";
+
+		function Controller() {
+
+            var vm = this;
+
+        }
+
+		angular.module("umbraco").controller("My.Controller", Controller);
+
+	})();
+
+ +@param {string=} language Language of the code snippet, e.g csharp, html, css. +**/ + + +(function () { + 'use strict'; + + var umbCodeSnippet = { + templateUrl: 'views/components/umb-code-snippet.html', + controller: UmbCodeSnippetController, + controllerAs: 'vm', + transclude: true, + bindings: { + language: '<' + } + }; + + function UmbCodeSnippetController($timeout) { + + const vm = this; + + vm.page = {}; + + vm.$onInit = onInit; + vm.copySuccess = copySuccess; + vm.copyError = copyError; + + function onInit() { + vm.guid = String.CreateGuid(); + + if (vm.language) + { + switch (vm.language.toLowerCase()) { + case "csharp": + case "c#": + vm.language = "C#"; + break; + case "html": + vm.language = "HTML"; + break; + case "css": + vm.language = "CSS"; + break; + case "javascript": + vm.language = "JavaScript"; + break; + } + } + + } + + // copy to clip board success + function copySuccess() { + if (vm.page.copyCodeButtonState !== "success") { + $timeout(function () { + vm.page.copyCodeButtonState = "success"; + }); + $timeout(function () { + resetClipboardButtonState(); + }, 1000); + } + } + + // copy to clip board error + function copyError() { + if (vm.page.copyCodeButtonState !== "error") { + $timeout(function () { + vm.page.copyCodeButtonState = "error"; + }); + $timeout(function () { + resetClipboardButtonState(); + }, 1000); + } + } + + function resetClipboardButtonState() { + vm.page.copyCodeButtonState = "init"; + } + } + + angular.module('umbraco.directives').component('umbCodeSnippet', umbCodeSnippet); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index 3c9e300f92..a9b9cc52b1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -554,6 +554,7 @@ property.validation.patternMessage = propertyModel.validation.patternMessage; property.showOnMemberProfile = propertyModel.showOnMemberProfile; property.memberCanEdit = propertyModel.memberCanEdit; + property.isSensitiveData = propertyModel.isSensitiveData; property.isSensitiveValue = propertyModel.isSensitiveValue; property.allowCultureVariant = propertyModel.allowCultureVariant; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js index 1b011d2e19..1c4bf4d583 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js @@ -312,14 +312,7 @@ Use this directive to generate a thumbnail grid of media items. scope.onDetailsHover(item, $event, hover); } }; - - scope.clickEdit = function(item, $event) { - if (scope.onClickEdit) { - scope.onClickEdit({"item": item}) - $event.stopPropagation(); - } - }; - + var unbindItemsWatcher = scope.$watch('items', function(newValue, oldValue) { if (angular.isArray(newValue)) { activate(); @@ -341,8 +334,8 @@ Use this directive to generate a thumbnail grid of media items. onDetailsHover: "=", onClick: '=', onClickName: "=", - onClickEdit: "&?", - allowOnClickEdit: "@?", + allowOpenFolder: "=", + allowOpenFile: "=", filterBy: "=", itemMaxWidth: "@", itemMaxHeight: "@", diff --git a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/modelsbuildermanagement.resource.js similarity index 80% rename from src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.resource.js rename to src/Umbraco.Web.UI.Client/src/common/resources/modelsbuildermanagement.resource.js index 58ca77cbdb..ee3cd80c71 100644 --- a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/modelsbuildermanagement.resource.js @@ -1,4 +1,4 @@ -function modelsBuilderResource($q, $http, umbRequestHelper) { +function modelsBuilderManagementResource($q, $http, umbRequestHelper) { return { getModelsOutOfDateStatus: function () { @@ -20,4 +20,4 @@ } }; } -angular.module("umbraco.resources").factory("modelsBuilderResource", modelsBuilderResource); +angular.module("umbraco.resources").factory("modelsBuilderManagementResource", modelsBuilderManagementResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js index 5c3e6eb4c8..305e4a694d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js @@ -27,7 +27,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje generateModels: function () { var deferred = $q.defer(); - var modelsResource = $injector.has("modelsBuilderResource") ? $injector.get("modelsBuilderResource") : null; + var modelsResource = $injector.has("modelsBuilderManagementResource") ? $injector.get("modelsBuilderManagementResource") : null; var modelsBuilderEnabled = Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled; if (modelsBuilderEnabled && modelsResource) { modelsResource.buildModels().then(function(result) { @@ -48,7 +48,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje checkModelsBuilderStatus: function () { var deferred = $q.defer(); - var modelsResource = $injector.has("modelsBuilderResource") ? $injector.get("modelsBuilderResource") : null; + var modelsResource = $injector.has("modelsBuilderManagementResource") ? $injector.get("modelsBuilderManagementResource") : null; var modelsBuilderEnabled = (Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables && Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled === true); if (modelsBuilderEnabled && modelsResource) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index 3c64401933..1d80d3a3ed 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -359,6 +359,7 @@ When building a custom infinite editor view you can use the same components as a * * @param {Object} editor rendering options * @param {Boolean} editor.multiPicker Pick one or multiple items + * @param {Int} editor.startNodeId Set the startnode of the picker (optional) * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object * @param {Function} editor.close Callback function when the close button is clicked. * @@ -366,7 +367,7 @@ When building a custom infinite editor view you can use the same components as a */ function contentPicker(editor) { editor.view = "views/common/infiniteeditors/treepicker/treepicker.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; editor.section = "content"; editor.treeAlias = "content"; open(editor); @@ -389,7 +390,7 @@ When building a custom infinite editor view you can use the same components as a */ function contentTypePicker(editor) { editor.view = "views/common/infiniteeditors/treepicker/treepicker.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; editor.section = "settings"; editor.treeAlias = "documentTypes"; open(editor); @@ -412,7 +413,7 @@ When building a custom infinite editor view you can use the same components as a */ function mediaTypePicker(editor) { editor.view = "views/common/infiniteeditors/treepicker/treepicker.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; editor.section = "settings"; editor.treeAlias = "mediaTypes"; open(editor); @@ -435,7 +436,7 @@ When building a custom infinite editor view you can use the same components as a */ function memberTypePicker(editor) { editor.view = "views/common/infiniteeditors/treepicker/treepicker.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; editor.section = "settings"; editor.treeAlias = "memberTypes"; open(editor); @@ -456,7 +457,7 @@ When building a custom infinite editor view you can use the same components as a function copy(editor) { editor.view = "views/common/infiniteeditors/copy/copy.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -476,7 +477,7 @@ When building a custom infinite editor view you can use the same components as a function move(editor) { editor.view = "views/common/infiniteeditors/move/move.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -494,7 +495,7 @@ When building a custom infinite editor view you can use the same components as a function embed(editor) { editor.view = "views/common/infiniteeditors/embed/embed.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -513,7 +514,7 @@ When building a custom infinite editor view you can use the same components as a function rollback(editor) { editor.view = "views/common/infiniteeditors/rollback/rollback.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -533,7 +534,7 @@ When building a custom infinite editor view you can use the same components as a */ function linkPicker(editor) { editor.view = "views/common/infiniteeditors/linkpicker/linkpicker.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -564,6 +565,7 @@ When building a custom infinite editor view you can use the same components as a * @description * Opens a media picker in infinite editing, the submit callback returns an array of selected media items * @param {Object} editor rendering options + * @param {Int} editor.startNodeId Set the startnode of the picker (optional) * @param {Boolean} editor.multiPicker Pick one or multiple items * @param {Boolean} editor.onlyImages Only display files that have an image file-extension * @param {Boolean} editor.disableFolderSelect Disable folder selection @@ -575,7 +577,7 @@ When building a custom infinite editor view you can use the same components as a */ function mediaPicker(editor) { editor.view = "views/common/infiniteeditors/mediapicker/mediapicker.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; editor.updatedMediaNodes = []; open(editor); } @@ -596,7 +598,7 @@ When building a custom infinite editor view you can use the same components as a */ function iconPicker(editor) { editor.view = "views/common/infiniteeditors/iconpicker/iconpicker.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -608,8 +610,11 @@ When building a custom infinite editor view you can use the same components as a * @description * Opens the document type editor in infinite editing, the submit callback returns the alias of the saved document type. * @param {Object} editor rendering options - * @param {Callback} editor.id Indicates the ID of the document type to be edited. Alternatively the ID may be set to `-1` in combination with `create` being set to `true` to open the document type editor for creating a new document type. - * @param {Callback} editor.create Set to `true` to open the document type editor for creating a new document type. + * @param {Number} editor.id Indicates the ID of the document type to be edited. Alternatively the ID may be set to `-1` in combination with `create` being set to `true` to open the document type editor for creating a new document type. + * @param {Boolean} editor.create Set to `true` to open the document type editor for creating a new document type. + * @param {Boolean} editor.noTemplate If `true` and in combination with `create` being set to `true`, the document type editor will not create a corresponding template by default. This is similar to selecting the "Document Type without a template" in the Create dialog. + * @param {Boolean} editor.isElement If `true` and in combination with `create` being set to `true`, the "Is an Element type" option will be selected by default in the document type editor. + * @param {Boolean} editor.allowVaryByCulture If `true` and in combination with `create`, the "Allow varying by culture" option will be selected by default in the document type editor. * @param {Callback} editor.submit Submits the editor. * @param {Callback} editor.close Closes the editor. * @returns {Object} editor object @@ -636,6 +641,23 @@ When building a custom infinite editor view you can use the same components as a open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#memberTypeEditor + * @methodOf umbraco.services.editorService + * + * @description + * Opens the member type editor in infinite editing, the submit callback returns the saved member type + * @param {Object} editor rendering options + * @param {Callback} editor.submit Submits the editor + * @param {Callback} editor.close Closes the editor + * @returns {Object} editor object + */ + function memberTypeEditor(editor) { + editor.view = "views/membertypes/edit.html"; + open(editor); + } + /** * @ngdoc method * @name umbraco.services.editorService#queryBuilder @@ -670,7 +692,7 @@ When building a custom infinite editor view you can use the same components as a */ function treePicker(editor) { editor.view = "views/common/infiniteeditors/treepicker/treepicker.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -688,7 +710,7 @@ When building a custom infinite editor view you can use the same components as a */ function nodePermissions(editor) { editor.view = "views/common/infiniteeditors/nodepermissions/nodepermissions.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -706,7 +728,7 @@ When building a custom infinite editor view you can use the same components as a */ function insertCodeSnippet(editor) { editor.view = "views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -724,7 +746,7 @@ When building a custom infinite editor view you can use the same components as a */ function userGroupPicker(editor) { editor.view = "views/common/infiniteeditors/usergrouppicker/usergrouppicker.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -760,7 +782,7 @@ When building a custom infinite editor view you can use the same components as a */ function sectionPicker(editor) { editor.view = "views/common/infiniteeditors/sectionpicker/sectionpicker.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -778,7 +800,7 @@ When building a custom infinite editor view you can use the same components as a */ function insertField(editor) { editor.view = "views/common/infiniteeditors/insertfield/insertfield.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -796,7 +818,7 @@ When building a custom infinite editor view you can use the same components as a */ function templateSections(editor) { editor.view = "views/common/infiniteeditors/templatesections/templatesections.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -814,7 +836,7 @@ When building a custom infinite editor view you can use the same components as a */ function userPicker(editor) { editor.view = "views/common/infiniteeditors/userpicker/userpicker.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -836,7 +858,7 @@ When building a custom infinite editor view you can use the same components as a */ function itemPicker(editor) { editor.view = "views/common/infiniteeditors/itempicker/itempicker.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -854,7 +876,7 @@ When building a custom infinite editor view you can use the same components as a */ function macroPicker(editor) { editor.view = "views/common/infiniteeditors/macropicker/macropicker.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -874,7 +896,7 @@ When building a custom infinite editor view you can use the same components as a */ function memberGroupPicker(editor) { editor.view = "views/common/infiniteeditors/membergrouppicker/membergrouppicker.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; open(editor); } @@ -895,7 +917,7 @@ When building a custom infinite editor view you can use the same components as a */ function memberPicker(editor) { editor.view = "views/common/infiniteeditors/treepicker/treepicker.html"; - editor.size = "small"; + if (!editor.size) editor.size = "small"; editor.section = "member"; editor.treeAlias = "member"; open(editor); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js b/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js index 8fe6761c94..41614a3bee 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js @@ -16,7 +16,7 @@ function fileManager($rootScope) { var mgr = { /** * @ngdoc function - * @name umbraco.services.fileManager#addFiles + * @name umbraco.services.fileManager#setFiles * @methodOf umbraco.services.fileManager * @function * diff --git a/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js index f66b0b0ed0..31375c5c56 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js @@ -1,323 +1,325 @@ -// This service was based on OpenJS library available in BSD License -// http://www.openjs.com/scripts/events/keyboard_shortcuts/index.php - -function keyboardService($window, $timeout) { - - var keyboardManagerService = {}; - - var defaultOpt = { - 'type': 'keydown', - 'propagate': false, - 'inputDisabled': false, - 'target': $window.document, - 'keyCode': false - }; - - // Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken - var shift_nums = { - "`": "~", - "1": "!", - "2": "@", - "3": "#", - "4": "$", - "5": "%", - "6": "^", - "7": "&", - "8": "*", - "9": "(", - "0": ")", - "-": "_", - "=": "+", - ";": ":", - "'": "\"", - ",": "<", - ".": ">", - "/": "?", - "\\": "|" - }; - - // Special Keys - and their codes - var special_keys = { - 'esc': 27, - 'escape': 27, - 'tab': 9, - 'space': 32, - 'return': 13, - 'enter': 13, - 'backspace': 8, - - 'scrolllock': 145, - 'scroll_lock': 145, - 'scroll': 145, - 'capslock': 20, - 'caps_lock': 20, - 'caps': 20, - 'numlock': 144, - 'num_lock': 144, - 'num': 144, - - 'pause': 19, - 'break': 19, - - 'insert': 45, - 'home': 36, - 'delete': 46, - 'end': 35, - - 'pageup': 33, - 'page_up': 33, - 'pu': 33, - - 'pagedown': 34, - 'page_down': 34, - 'pd': 34, - - 'left': 37, - 'up': 38, - 'right': 39, - 'down': 40, - - 'f1': 112, - 'f2': 113, - 'f3': 114, - 'f4': 115, - 'f5': 116, - 'f6': 117, - 'f7': 118, - 'f8': 119, - 'f9': 120, - 'f10': 121, - 'f11': 122, - 'f12': 123 - }; - - var isMac = navigator.platform.toUpperCase().indexOf('MAC')>=0; - - // The event handler for bound element events - function eventHandler(e) { - e = e || $window.event; - - var code, k; - - // Find out which key is pressed - if (e.keyCode) - { - code = e.keyCode; - } - else if (e.which) { - code = e.which; - } - - var character = String.fromCharCode(code).toLowerCase(); - - if (code === 188){character = ",";} // If the user presses , when the type is onkeydown - if (code === 190){character = ".";} // If the user presses , when the type is onkeydown - - var propagate = true; - - //Now we need to determine which shortcut this event is for, we'll do this by iterating over each - //registered shortcut to find the match. We use Find here so that the loop exits as soon - //as we've found the one we're looking for - _.find(_.keys(keyboardManagerService.keyboardEvent), function(key) { - - var shortcutLabel = key; - var shortcutVal = keyboardManagerService.keyboardEvent[key]; - - // Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked - var kp = 0; - - // Some modifiers key - var modifiers = { - shift: { - wanted: false, - pressed: e.shiftKey ? true : false - }, - ctrl: { - wanted: false, - pressed: e.ctrlKey ? true : false - }, - alt: { - wanted: false, - pressed: e.altKey ? true : false - }, - meta: { //Meta is Mac specific - wanted: false, - pressed: e.metaKey ? true : false - } - }; - - var keys = shortcutLabel.split("+"); - var opt = shortcutVal.opt; - var callback = shortcutVal.callback; - - // Foreach keys in label (split on +) - var l = keys.length; - for (var i = 0; i < l; i++) { - - var k = keys[i]; - switch (k) { - case 'ctrl': - case 'control': - kp++; - modifiers.ctrl.wanted = true; - break; - case 'shift': - case 'alt': - case 'meta': - kp++; - modifiers[k].wanted = true; - break; - } - - if (k.length > 1) { // If it is a special key - if (special_keys[k] === code) { - kp++; - } - } - else if (opt['keyCode']) { // If a specific key is set into the config - if (opt['keyCode'] === code) { - kp++; - } - } - else { // The special keys did not match - if (character === k) { - kp++; - } - else { - if (shift_nums[character] && e.shiftKey) { // Stupid Shift key bug created by using lowercase - character = shift_nums[character]; - if (character === k) { - kp++; - } - } - } - } - - } //for end - - if (kp === keys.length && - modifiers.ctrl.pressed === modifiers.ctrl.wanted && - modifiers.shift.pressed === modifiers.shift.wanted && - modifiers.alt.pressed === modifiers.alt.wanted && - modifiers.meta.pressed === modifiers.meta.wanted) { - - //found the right callback! - - // Disable event handler when focus input and textarea - if (opt['inputDisabled']) { - var elt; - if (e.target) { - elt = e.target; - } else if (e.srcElement) { - elt = e.srcElement; - } - - if (elt.nodeType === 3) { elt = elt.parentNode; } - if (elt.tagName === 'INPUT' || elt.tagName === 'TEXTAREA') { - //This exits the Find loop - return true; - } - } - - $timeout(function () { - callback(e); - }, 1); - - if (!opt['propagate']) { // Stop the event - propagate = false; - } - - //This exits the Find loop - return true; - } - - //we haven't found one so continue looking - return false; - - }); - - // Stop the event if required - if (!propagate) { - // e.cancelBubble is supported by IE - this will kill the bubbling process. - e.cancelBubble = true; - e.returnValue = false; - - // e.stopPropagation works in Firefox. - if (e.stopPropagation) { - e.stopPropagation(); - e.preventDefault(); - } - return false; - } - } - - // Store all keyboard combination shortcuts - keyboardManagerService.keyboardEvent = {}; - - // Add a new keyboard combination shortcut - keyboardManagerService.bind = function (label, callback, opt) { - - //replace ctrl key with meta key - if(isMac && label !== "ctrl+space"){ - label = label.replace("ctrl","meta"); - } - - var elt; - // Initialize opt object - opt = angular.extend({}, defaultOpt, opt); - label = label.toLowerCase(); - elt = opt.target; - if(typeof opt.target === 'string'){ - elt = document.getElementById(opt.target); - } - - //Ensure we aren't double binding to the same element + type otherwise we'll end up multi-binding - // and raising events for now reason. So here we'll check if the event is already registered for the element - var boundValues = _.values(keyboardManagerService.keyboardEvent); - var found = _.find(boundValues, function (i) { - return i.target === elt && i.event === opt['type']; - }); - - // Store shortcut - keyboardManagerService.keyboardEvent[label] = { - 'callback': callback, - 'target': elt, - 'opt': opt - }; - - if (!found) { - //Attach the function with the event - if (elt.addEventListener) { - elt.addEventListener(opt['type'], eventHandler, false); - } else if (elt.attachEvent) { - elt.attachEvent('on' + opt['type'], eventHandler); - } else { - elt['on' + opt['type']] = eventHandler; - } - } - - }; - // Remove the shortcut - just specify the shortcut and I will remove the binding - keyboardManagerService.unbind = function (label) { - label = label.toLowerCase(); - var binding = keyboardManagerService.keyboardEvent[label]; - delete(keyboardManagerService.keyboardEvent[label]); - - if(!binding){return;} - - var type = binding['event'], - elt = binding['target'], - callback = binding['callback']; - - if(elt.detachEvent){ - elt.detachEvent('on' + type, callback); - }else if(elt.removeEventListener){ - elt.removeEventListener(type, callback, false); - }else{ - elt['on'+type] = false; - } - }; - // - - return keyboardManagerService; -} angular.module('umbraco.services').factory('keyboardService', ['$window', '$timeout', keyboardService]); \ No newline at end of file +// This service was based on OpenJS library available in BSD License +// http://www.openjs.com/scripts/events/keyboard_shortcuts/index.php + +function keyboardService($window, $timeout) { + + var keyboardManagerService = {}; + + var defaultOpt = { + 'type': 'keydown', + 'propagate': false, + 'inputDisabled': false, + 'target': $window.document, + 'keyCode': false + }; + + // Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken + var shift_nums = { + "`": "~", + "1": "!", + "2": "@", + "3": "#", + "4": "$", + "5": "%", + "6": "^", + "7": "&", + "8": "*", + "9": "(", + "0": ")", + "-": "_", + "=": "+", + ";": ":", + "'": "\"", + ",": "<", + ".": ">", + "/": "?", + "\\": "|" + }; + + // Special Keys - and their codes + var special_keys = { + 'esc': 27, + 'escape': 27, + 'tab': 9, + 'space': 32, + 'return': 13, + 'enter': 13, + 'backspace': 8, + + 'scrolllock': 145, + 'scroll_lock': 145, + 'scroll': 145, + 'capslock': 20, + 'caps_lock': 20, + 'caps': 20, + 'numlock': 144, + 'num_lock': 144, + 'num': 144, + + 'pause': 19, + 'break': 19, + + 'insert': 45, + 'home': 36, + 'delete': 46, + 'end': 35, + + 'pageup': 33, + 'page_up': 33, + 'pu': 33, + + 'pagedown': 34, + 'page_down': 34, + 'pd': 34, + + 'left': 37, + 'up': 38, + 'right': 39, + 'down': 40, + + 'f1': 112, + 'f2': 113, + 'f3': 114, + 'f4': 115, + 'f5': 116, + 'f6': 117, + 'f7': 118, + 'f8': 119, + 'f9': 120, + 'f10': 121, + 'f11': 122, + 'f12': 123 + }; + + var isMac = navigator.platform.toUpperCase().indexOf('MAC')>=0; + + // The event handler for bound element events + function eventHandler(e) { + e = e || $window.event; + + var code, k; + + // Find out which key is pressed + if (e.keyCode) + { + code = e.keyCode; + } + else if (e.which) { + code = e.which; + } + + var character = String.fromCharCode(code).toLowerCase(); + + if (code === 188){character = ",";} // If the user presses , when the type is onkeydown + if (code === 190){character = ".";} // If the user presses , when the type is onkeydown + + var propagate = true; + + //Now we need to determine which shortcut this event is for, we'll do this by iterating over each + //registered shortcut to find the match. We use Find here so that the loop exits as soon + //as we've found the one we're looking for + _.find(_.keys(keyboardManagerService.keyboardEvent), function(key) { + + var shortcutLabel = key; + var shortcutVal = keyboardManagerService.keyboardEvent[key]; + + // Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked + var kp = 0; + + // Some modifiers key + var modifiers = { + shift: { + wanted: false, + pressed: e.shiftKey ? true : false + }, + ctrl: { + wanted: false, + pressed: e.ctrlKey ? true : false + }, + alt: { + wanted: false, + pressed: e.altKey ? true : false + }, + meta: { //Meta is Mac specific + wanted: false, + pressed: e.metaKey ? true : false + } + }; + + var keys = shortcutLabel.split("+"); + var opt = shortcutVal.opt; + var callback = shortcutVal.callback; + + // Foreach keys in label (split on +) + var l = keys.length; + for (var i = 0; i < l; i++) { + + var k = keys[i]; + switch (k) { + case 'ctrl': + case 'control': + kp++; + modifiers.ctrl.wanted = true; + break; + case 'shift': + case 'alt': + case 'meta': + kp++; + modifiers[k].wanted = true; + break; + } + + if (k.length > 1) { // If it is a special key + if (special_keys[k] === code) { + kp++; + } + } + else if (opt['keyCode']) { // If a specific key is set into the config + if (opt['keyCode'] === code) { + kp++; + } + } + else { // The special keys did not match + if (character === k) { + kp++; + } + else { + if (shift_nums[character] && e.shiftKey) { // Stupid Shift key bug created by using lowercase + character = shift_nums[character]; + if (character === k) { + kp++; + } + } + } + } + + } //for end + + if (kp === keys.length && + modifiers.ctrl.pressed === modifiers.ctrl.wanted && + modifiers.shift.pressed === modifiers.shift.wanted && + modifiers.alt.pressed === modifiers.alt.wanted && + modifiers.meta.pressed === modifiers.meta.wanted) { + + //found the right callback! + + // Disable event handler when focus input and textarea + if (opt['inputDisabled']) { + var elt; + if (e.target) { + elt = e.target; + } else if (e.srcElement) { + elt = e.srcElement; + } + + if (elt.nodeType === 3) { elt = elt.parentNode; } + if (elt.tagName === 'INPUT' || elt.tagName === 'TEXTAREA' || elt.hasAttribute('disable-hotkeys')) { + //This exits the Find loop + return true; + } + } + + $timeout(function () { + callback(e); + }, 1); + + if (!opt['propagate']) { // Stop the event + propagate = false; + } + + //This exits the Find loop + return true; + } + + //we haven't found one so continue looking + return false; + + }); + + // Stop the event if required + if (!propagate) { + // e.cancelBubble is supported by IE - this will kill the bubbling process. + e.cancelBubble = true; + e.returnValue = false; + + // e.stopPropagation works in Firefox. + if (e.stopPropagation) { + e.stopPropagation(); + e.preventDefault(); + } + return false; + } + } + + // Store all keyboard combination shortcuts + keyboardManagerService.keyboardEvent = {}; + + // Add a new keyboard combination shortcut + keyboardManagerService.bind = function (label, callback, opt) { + + //replace ctrl key with meta key + if(isMac && label !== "ctrl+space"){ + label = label.replace("ctrl","meta"); + } + + var elt; + // Initialize opt object + opt = angular.extend({}, defaultOpt, opt); + label = label.toLowerCase(); + elt = opt.target; + if(typeof opt.target === 'string'){ + elt = document.getElementById(opt.target); + } + + //Ensure we aren't double binding to the same element + type otherwise we'll end up multi-binding + // and raising events for now reason. So here we'll check if the event is already registered for the element + var boundValues = _.values(keyboardManagerService.keyboardEvent); + var found = _.find(boundValues, function (i) { + return i.target === elt && i.event === opt['type']; + }); + + // Store shortcut + keyboardManagerService.keyboardEvent[label] = { + 'callback': callback, + 'target': elt, + 'opt': opt + }; + + if (!found) { + //Attach the function with the event + if (elt.addEventListener) { + elt.addEventListener(opt['type'], eventHandler, false); + } else if (elt.attachEvent) { + elt.attachEvent('on' + opt['type'], eventHandler); + } else { + elt['on' + opt['type']] = eventHandler; + } + } + + }; + // Remove the shortcut - just specify the shortcut and I will remove the binding + keyboardManagerService.unbind = function (label) { + label = label.toLowerCase(); + var binding = keyboardManagerService.keyboardEvent[label]; + delete(keyboardManagerService.keyboardEvent[label]); + + if(!binding){return;} + + var type = binding['event'], + elt = binding['target'], + callback = binding['callback']; + + if(elt.detachEvent){ + elt.detachEvent('on' + type, callback); + }else if(elt.removeEventListener){ + elt.removeEventListener(type, callback, false); + }else{ + elt['on'+type] = false; + } + }; + // + + return keyboardManagerService; +} + +angular.module('umbraco.services').factory('keyboardService', ['$window', '$timeout', keyboardService]); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js b/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js index c123ac6cea..e5701b9de0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js @@ -148,7 +148,7 @@ angular.module('umbraco.services') break; case 1: //info - this.success(args.header, args.message); + this.info(args.header, args.message); break; case 2: //error @@ -297,4 +297,4 @@ angular.module('umbraco.services') }; return service; -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index b0941bd5ad..61e3ae90ec 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -488,7 +488,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s * @methodOf umbraco.services.tinyMceService * * @description - * Creates the umbrco insert embedded media tinymce plugin + * Creates the umbraco insert embedded media tinymce plugin * * @param {Object} editor the TinyMCE editor instance */ @@ -575,7 +575,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s * @methodOf umbraco.services.tinyMceService * * @description - * Creates the umbrco insert media tinymce plugin + * Creates the umbraco insert media tinymce plugin * * @param {Object} editor the TinyMCE editor instance */ @@ -705,7 +705,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s * @methodOf umbraco.services.tinyMceService * * @description - * Creates the insert umbrco macro tinymce plugin + * Creates the insert umbraco macro tinymce plugin * * @param {Object} editor the TinyMCE editor instance */ diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html index 6b71f1db9e..fc870858cf 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html @@ -15,11 +15,11 @@
-

Great!, no need to configure anything then, you simply click the continue button below to continue to the next step

+

Great! No need to configure anything, you can simply click the continue button below to continue to the next step

- What is the exact connectionstring we should use? + What is the exact connection string we should use?
@@ -64,7 +64,7 @@
Enter the database user name
diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/permissionsreport.html b/src/Umbraco.Web.UI.Client/src/installer/steps/permissionsreport.html index ad9b1c9cc1..0c78e211dc 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/permissionsreport.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/permissionsreport.html @@ -1,8 +1,8 @@ 
-

Your permission settings are not ready for umbraco

+

Your permission settings are not ready for Umbraco

- In order to run umbraco, you'll need to update your permission settings. - Detailed information about the correct file & folder permissions for Umbraco can be found + In order to run Umbraco, you'll need to update your permission settings. + Detailed information about the correct file and folder permissions for Umbraco can be found here.

diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/version7upgradereport.html b/src/Umbraco.Web.UI.Client/src/installer/steps/version7upgradereport.html index df1e58d737..f30788e858 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/version7upgradereport.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/version7upgradereport.html @@ -2,11 +2,11 @@

Major version upgrade from {{installer.current.model.currentVersion}} to {{installer.current.model.newVersion}}

There were {{installer.current.model.errors.length}} issues detected

- The following compatibility issues were found. If you continue all non-compatible property editors will be converted to a Readonly/Label. + The following compatibility issues were found. If you continue, all non-compatible property editors will be converted to a Readonly/Label. You will be able to change the property editor to a compatible type manually by editing the data type after installation.

- Otherwise if you choose not to proceed you will need to fix the errors listed below. + Otherwise, if you choose not to proceed, you will need to fix the errors listed below. Refer to v{{installer.current.model.newVersion}} upgrade instructions for full details.

@@ -19,4 +19,4 @@

-
\ No newline at end of file +
diff --git a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less index 1a04dd10c8..939366d5ac 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less @@ -1,3 +1,7 @@ +*:focus { + outline-color: @ui-outline; +} + .umb-outline { &:focus { outline:none; @@ -10,7 +14,28 @@ left: 0; right: 0; border-radius: 3px; - box-shadow: 0 0 2px @blueMid, inset 0 0 2px 1px @blueMid; + box-shadow: 0 0 2px 0px @ui-outline, inset 0 0 2px 2px @ui-outline; } } + + &.umb-outline--surrounding { + &:focus { + .tabbing-active &::after { + top: -6px; + bottom: -6px; + left: -6px; + right: -6px; + border-radius: 9px; + } + } + } + + &.umb-outline--thin { + &:focus { + .tabbing-active &::after { + box-shadow: 0 0 2px @ui-outline, inset 0 0 2px 1px @ui-outline; + } + } + } + } diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index f6490fc79b..b5e032f9fb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -132,6 +132,7 @@ @import "components/umb-content-grid.less"; @import "components/umb-contextmenu.less"; @import "components/umb-layout-selector.less"; +@import "components/umb-mini-search.less"; @import "components/tooltip/umb-tooltip.less"; @import "components/tooltip/umb-tooltip-list.less"; @import "components/overlays/umb-overlay-backdrop.less"; @@ -140,6 +141,7 @@ @import "components/umb-empty-state.less"; @import "components/umb-property-editor.less"; @import "components/umb-property-actions.less"; +@import "components/umb-code-snippet.less"; @import "components/umb-color-swatches.less"; @import "components/check-circle.less"; @import "components/umb-file-icon.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/buttons.less b/src/Umbraco.Web.UI.Client/src/less/buttons.less index 85532f4231..2b50b60ae8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/buttons.less +++ b/src/Umbraco.Web.UI.Client/src/less/buttons.less @@ -23,8 +23,7 @@ border-radius: 3px; // Hover/focus state - &:hover, - &:focus { + &:hover { background: @btnBackgroundHighlight; color: @gray-4; background-position: 0 -15px; @@ -35,11 +34,6 @@ .transition(background-position .1s linear); } - // Focus state for keyboard and accessibility - &:focus { - .tab-focus(); - } - // Active state &.active, &:active { @@ -54,7 +48,7 @@ &:disabled:hover { cursor: default; border-color: @btnBorder; - .opacity(65); + .opacity(80); .box-shadow(none); } @@ -219,7 +213,7 @@ input[type="button"] { } // Made for Umbraco, 2019, used for buttons that has to stand back. .btn-white { - .buttonBackground(@btnWhiteBackground, @btnWhiteBackgroundHighlight, @btnWhiteType, @btnWhiteTypeHover); + .buttonBackground(@btnWhiteBackground, @btnWhiteBackgroundHighlight, @btnWhiteType, @btnWhiteTypeHover, @gray-10, @gray-7); } // Inverse appears as dark gray .btn-inverse { @@ -230,8 +224,7 @@ input[type="button"] { .buttonBackground(@btnNeutralBackground, @btnNeutralBackgroundHighlight); color: @gray-5; // Hover/focus state - &:hover, - &:focus { + &:hover { color: @gray-5; } @@ -261,18 +254,18 @@ input[type="button"] { .btn-outline { border: 1px solid; border-color: @gray-7; - background: @white; + background: transparent; color: @blueExtraDark; padding: 5px 13px; - transition: all .2s linear; + transition: border-color .12s linear, color .12s linear; + font-weight: 600; } -.btn-outline:hover, -.btn-outline:focus, -.btn-outline:active { +.btn-outline:hover { border-color: @ui-light-type-hover; color: @ui-light-type-hover; - background: @white; + background: transparent; + transition: border-color .12s linear, color .12s linear; } // Cross-browser Jank @@ -309,14 +302,12 @@ input[type="submit"].btn { color: @linkColor; .border-radius(0); } -.btn-link:hover, -.btn-link:focus { +.btn-link:hover { color: @linkColorHover; text-decoration: underline; background-color: transparent; } -.btn-link[disabled]:hover, -.btn-link[disabled]:focus { +.btn-link[disabled]:hover { color: @gray-4; text-decoration: none; } @@ -324,8 +315,7 @@ input[type="submit"].btn { // Make a reverse type of a button link .btn-link-reverse{ text-decoration:underline; - &:hover, - &:focus{ + &:hover { text-decoration:none; } } @@ -362,7 +352,7 @@ input[type="submit"].btn { outline: 0; -webkit-appearance: none; - &:hover, &:focus { + &:hover { color: @ui-icon-hover; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less index 064ad67438..5c77a15ec7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less @@ -86,6 +86,7 @@ } .umb-help-badge__title { + display: block; font-size: 15px; font-weight: bold; color: @black; @@ -160,6 +161,9 @@ border-radius: 0; border-bottom: 1px solid @gray-9; padding: 10px; + background: transparent; + width:100%; + border: 0 none; } .umb-help-list-item:last-child { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less index 7d91783e32..4e3741905f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less @@ -19,13 +19,13 @@ box-sizing: border-box; color: @ui-option-type; width: 100%; + outline-offset: -3px; } .umb-language-picker__expand { font-size: 14px; } -.umb-language-picker__toggle:focus, .umb-language-picker__toggle:hover { background: @ui-option-hover; color:@ui-option-type-hover; @@ -54,10 +54,10 @@ font-size: 14px; width: 100%; text-align: left; + outline-offset: -3px; } -.umb-language-picker__dropdown-item:hover, -.umb-language-picker__dropdown-item:focus { +.umb-language-picker__dropdown-item:hover { background: @ui-option-hover; text-decoration: none; color:@ui-option-type-hover; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less index 33a723a3f7..42403c65b1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less @@ -112,3 +112,10 @@ .umb-tour-is-visible .umb-backdrop { z-index: @zindexTourBackdrop; } + +.umb-tour__popover .underline{ + font-size: 13px; + background: transparent; + border: none; + padding: 0; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less index 7fc965a8fa..4127c2201c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less @@ -8,21 +8,6 @@ position: relative; } -.umb-button__button:focus { - outline: none; - .tabbing-active &:after { - content: ''; - position: absolute; - z-index: 10000; - top: 0; - bottom: 0; - left: 0; - right: 0; - border-radius: 3px; - box-shadow: 0 0 2px @blueMid, inset 0 0 2px 1px @blueMid; - } -} - .umb-button__content { opacity: 1; transition: opacity 0.25s ease; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor.less index 85fcc249f9..bc84b0d35e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor.less @@ -164,6 +164,7 @@ a.umb-editor-header__close-split-view:hover { /* variant switcher */ .umb-variant-switcher__toggle { + position: relative; display: flex; align-items: center; padding: 0 10px; @@ -173,6 +174,8 @@ a.umb-editor-header__close-split-view:hover { text-decoration: none !important; font-size: 13px; color: @ui-action-discreet-type; + background: transparent; + border: none; max-width: 50%; white-space: nowrap; @@ -185,7 +188,7 @@ a.umb-editor-header__close-split-view:hover { } } -a.umb-variant-switcher__toggle { +button.umb-variant-switcher__toggle { transition: color 0.2s ease-in-out; &:hover { //background-color: @gray-10; @@ -242,8 +245,7 @@ a.umb-variant-switcher__toggle { border-left: 4px solid @ui-active; } -.umb-variant-switcher__item:hover, -.umb-variant-switcher__item:focus { +.umb-variant-switcher__item:hover { outline: none; } @@ -267,7 +269,7 @@ a.umb-variant-switcher__toggle { align-items: center; justify-content: center; margin-left: 5px; - top: -6px; + top: -3px; width: 14px; height: 14px; border-radius: 7px; @@ -285,8 +287,10 @@ a.umb-variant-switcher__toggle { flex: 1; cursor: pointer; padding-top: 6px !important; - padding-bottom: 6px !important; - border-left: 2px solid transparent; + padding-bottom: 6px !important; + background-color: transparent; + border: none; + border-left: 2px solid transparent; } .umb-variant-switcher__name { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less index 1217441f4e..4ebfa94b6f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less @@ -24,8 +24,9 @@ .umb-editor-sub-header.--state-selection { padding-left: 10px; padding-right: 10px; - background-color: @pinkLight; - border-color: @pinkLight; + background-color: @ui-selected-border; + border-color: @ui-selected-border; + color: @white; border-radius: 3px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less index 6859280680..d2a3bdedb1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less @@ -1,6 +1,6 @@ .umb-editors { .absolute(); - overflow: hidden; + overflow: hidden; .umb-editor { box-shadow: 0px 0 30px 0 rgba(0,0,0,.3); @@ -16,7 +16,7 @@ transform: none; will-change: transform; transition: transform 400ms ease-in-out; - + &.umb-editor--moveRight { transform: translateX(110%); } @@ -28,7 +28,7 @@ will-change: auto; transition: display 0s 320ms; } - + &--level0 { transform: none; } @@ -43,11 +43,11 @@ .umb-editor--level@{i} { transform: translateX(@x); } - + .umb-editor--n@{i} { right:@x; } - + .level-loop(@i - 1); } @@ -62,18 +62,18 @@ .umb-editor { @size: extract(extract(@editorSizes, @iterator), 1); @value: extract(extract(@editorSizes, @iterator), 2); - + &--@{size} { width: @value; will-change: transform; left: auto; - + .umb-editor--container { max-width: @value; } } } - + .create-editor-sizes(@iterator + 1); } @@ -94,3 +94,14 @@ opacity: 1; transition: opacity 320ms 20ms, visibility 0s; } + +.umb-editor--trashed-message { + background:@errorBackground; + color:@errorText; + padding:10px; + margin-bottom:20px; + + i { + margin-right:5px; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less index 609cf0af3d..eb8740b385 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -66,7 +66,7 @@ flex-shrink: 1; flex-basis: auto; position: relative; - padding: 20px; + padding: 30px; background: @white; max-height: calc(100vh - 170px); overflow-y: auto; @@ -117,7 +117,7 @@ .umb-overlay.umb-overlay-center .umb-overlay-drawer { border: none; background: transparent; - padding: 0 20px 20px; + padding: 0 30px 20px; } /* ---------- OVERLAY TARGET ---------- */ diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less index df01477880..4a483ce3f0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less @@ -4,16 +4,16 @@ width: auto; margin-top:1px; - .umb-tree-item__label { - user-select: none; - } - &:hover .umb-tree-item__arrow { visibility: visible; cursor: pointer } } +.umb-tree-item__label { + user-select: none; +} + .umb-tree-item__arrow { position: relative; margin-left: -16px; @@ -92,18 +92,6 @@ color: @blue; } - .umb-options { - - &:hover i { - opacity: .7; - } - - i { - background: @ui-active-type; - transition: opacity 120ms ease; - } - } - a, .umb-tree-icon, .umb-tree-item__arrow { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less index 0a0fb29eed..d06c15cd30 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -99,6 +99,7 @@ body.touch .umb-tree { .umb-tree-item__inner { border: 2px solid transparent; + overflow: visible; } .umb-tree-header { @@ -176,9 +177,25 @@ body.touch .umb-tree { cursor: pointer; border-radius: @baseBorderRadius; - &:hover { - background: @btnBackgroundHighlight; + i { + height: 5px !important; + width: 5px !important; + border-radius: 20px; + display: inline-block; + margin: 0 2px 0 0; + background: @ui-active-type; + + &:last-child { + margin: 0; + } } + &:hover { + background: rgba(255, 255, 255, .5); + i { + background: @ui-active-type-hover; + } + } + // NOTE - We're having to repeat ourselves here due to an .sr-only class appearing in umbraco/lib/font-awesome/css/font-awesome.min.css &.sr-only--hoverable:hover, &.sr-only--focusable:focus { @@ -193,19 +210,6 @@ body.touch .umb-tree { border-radius: 3px; } - i { - height: 5px !important; - width: 5px !important; - border-radius: 20px; - background: @black; - display: inline-block; - margin: 0 2px 0 0; - - &:last-child { - margin: 0; - } - } - .hide-options & { display: none !important; } @@ -289,9 +293,8 @@ body.touch .umb-tree { } .no-access { - .umb-tree-icon, - .root-link, - .umb-tree-item__label { + > .umb-tree-item__inner .umb-tree-icon, + > .umb-tree-item__inner .umb-tree-item__label { color: @gray-7; cursor: not-allowed; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less index 3c63d74a47..de678f9798 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less @@ -4,18 +4,32 @@ margin-left: 0; display: flex; flex-wrap: wrap; + user-select: none; } .umb-breadcrumbs__ancestor { display: flex; - min-height: 25px; + align-items: center; } .umb-breadcrumbs__action { + position: relative; background: transparent; border: 0 none; - padding: 0; - margin-top: -4px; + border-radius: 3px; + padding: 0 4px; + color: @ui-option-type; + + &.--current { + font-weight: bold; + pointer-events: none; + } + + &:hover { + color: @ui-option-type-hover; + background-color: @white; + } + } .umb-breadcrumbs__ancestor-link, @@ -26,6 +40,7 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + padding: 0 4px; } .umb-breadcrumbs__ancestor-link { @@ -39,13 +54,13 @@ .umb-breadcrumbs__separator { position: relative; top: 1px; - margin-left: 5px; - margin-right: 5px; + margin: 0 1px; + margin-top: -3px; color: @gray-7; } input.umb-breadcrumbs__add-ancestor { - height: 25px; - margin: 0 0 0 3px; + height: 24px; + margin: -2px 0 -2px 3px; width: 100px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less index 021fc8cc9b..f82e47bf88 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less @@ -2,21 +2,30 @@ border: 2px solid @white; width: 25px; height: 25px; - border: 1px solid @gray-7; + border: 1px solid @ui-action-discreet-border; border-radius: 3px; box-sizing: border-box; display: flex; justify-content: center; align-items: center; - color: @gray-7; + color: @ui-selected-type; cursor: pointer; font-size: 15px; + &:hover { + border-color:@ui-action-discreet-border-hover; + color: @ui-selected-type-hover; + } } .umb-checkmark--checked { - background: @ui-active; - border-color: @ui-active; + background: @ui-selected-border; + border-color: @ui-selected-border; color: @white; + &:hover { + background: @ui-selected-border-hover; + border-color: @ui-selected-border-hover; + color: @white; + } } .umb-checkmark--xs { @@ -45,4 +54,4 @@ width: 50px; height: 50px; font-size: 20px; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-code-snippet.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-code-snippet.less new file mode 100644 index 0000000000..b372841910 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-code-snippet.less @@ -0,0 +1,43 @@ +.umb-code-snippet { + + .umb-code-snippet__header { + box-sizing: content-box; + background-color: @gray-10; + display: flex; + flex-direction: row; + font-size: .8rem; + border: 1px solid @gray-8; + border-radius: 3px 3px 0 0; + border-bottom: 0; + margin-top: 16px; + min-height: 30px; + + .language { + display: flex; + align-items: center; + justify-content: flex-start; + flex-grow: 1; + padding: 2px 10px; + } + + button { + background-color: transparent; + border: none; + border-left: 1px solid @gray-8; + border-radius: 0; + color: #000; + + &:hover { + background-color: @grayLighter; + } + } + } + + .umb-code-snippet__content { + pre { + border-radius: 0 0 3px 3px; + overflow: auto; + white-space: nowrap; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less index f27e1e4ec8..622dcb8b0a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less @@ -29,15 +29,15 @@ border-radius: 5px; box-shadow: 0 0 4px 0 darken(@ui-selected-border, 20), inset 0 0 2px 0 darken(@ui-selected-border, 20); pointer-events: none; + transition: opacity 100ms; } - } } .umb-content-grid__item:hover { &::before { - opacity: .33; + opacity: .2; } } .umb-content-grid__item.-selected:hover { @@ -46,6 +46,7 @@ } } + .umb-content-grid__icon-container { height: 75px; display: flex; @@ -66,8 +67,10 @@ } .umb-content-grid__item-name { + position: relative; + padding: 5px; + margin: -5px -5px 15px -5px; font-weight: bold; - margin-bottom: 15px; line-height: 1.4em; display: inline-flex; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less index c26c89a478..6a6a8f9f5b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less @@ -4,6 +4,7 @@ &__action, > a { + position: relative; background: transparent; text-align: center; cursor: pointer; @@ -18,26 +19,20 @@ align-items: center; justify-content: center; height: calc(~'@{editorHeaderHeight}'- ~'1px'); // need to offset the 1px border-bottom on .umb-editor-header - avoids overflowing top of the container - position: relative; color: @ui-active-type; - &:focus, &:hover { color: @ui-active-type-hover !important; text-decoration: none; } - &:focus { - outline: none; - } - - &::after { + &::before { content: ""; + position: absolute; height: 0px; left: 8px; right: 8px; background-color: @ui-light-active-border; - position: absolute; bottom: 0; border-radius: 3px 3px 0 0; opacity: 0; @@ -47,14 +42,13 @@ &.is-active { color: @ui-light-active-type; - &::after { + &::before { opacity: 1; height: 4px; } } } - &__action:focus, &__action:active, & > a:active { .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)"); @@ -111,7 +105,6 @@ &__anchor_dropdown { // inherits from .dropdown-menu margin: 0; - overflow: hidden; // center align horizontal left: 50%; @@ -122,7 +115,7 @@ li { &.is-active a { - border-left-color: @ui-selected-border; + border-left-color: @ui-active; } a { @@ -192,4 +185,4 @@ &::after { background-color: @red; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less index e0dee5f266..76a4df0056 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less @@ -6,7 +6,7 @@ flex-wrap: wrap; align-items: center; position: relative; - padding: 0 !important; + padding: 0 0 0 26px !important; margin: 0; min-height: 22px; line-height: 22px; @@ -21,7 +21,6 @@ } &__text { - margin: 0 0 0 26px; position: relative; top: 1px; user-select: none; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid-selector.less index e25349f555..1ae476d584 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid-selector.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid-selector.less @@ -41,12 +41,17 @@ margin-top: 10px; } +button.umb-grid-selector__item { + width: 169px; + height: 194px; +} + .umb-grid-selector__item-icon { - font-size: 50px; - color: @gray-8; - display: block; - line-height: 50px; - margin-bottom: 15px; + font-size: 50px; + color: @gray-8; + display: block; + line-height: 50px; + margin-bottom: 15px; } .umb-grid-selector__item-label { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less index 9ebd6d6e5d..c0ac89622d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less @@ -6,7 +6,8 @@ .umb-layout-selector__active-layout { background: transparent; box-sizing: border-box; - border: 1px solid @inputBorder; + border: 1px solid @ui-action-discreet-border; + color: @ui-action-discreet-type; cursor: pointer; height: 30px; width: 30px; @@ -17,7 +18,8 @@ } .umb-layout-selector__active-layout:hover { - border-color: @inputBorderFocus; + border-color: @ui-action-discreet-border-hover; + color: @ui-action-discreet-type-hover; } .umb-layout-selector__dropdown { @@ -31,6 +33,7 @@ flex-direction: column; transform: translate(-50%,0); left: 50%; + border-radius: 3px; } .umb-layout-selector__dropdown-item { @@ -46,11 +49,11 @@ } .umb-layout-selector__dropdown-item:hover { - border: 1px solid @gray-8; + border: 1px solid @ui-action-discreet-border; } .umb-layout-selector__dropdown-item.-active { - border: 1px solid @blue; + border: 1px solid @ui-action-discreet-border-hover; } .umb-layout-selector__dropdown-item-icon, diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less index 05d91de9f7..5d6b7ad962 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less @@ -40,7 +40,8 @@ } } -.umb-media-grid__item.-selectable { +.umb-media-grid__item.-selectable, +.umb-media-grid__item.-folder {// If folders isnt selectable, they opens if clicked, therefor... cursor: pointer; } @@ -48,13 +49,32 @@ background-color: @white; } +.umb-media-grid__item.-folder { + + &.-selectable { + .media-grid-item-edit:hover .umb-media-grid__item-name, + .media-grid-item-edit:focus .umb-media-grid__item-name { + text-decoration: underline; + } + } + + &.-unselectable { + &:hover, &:focus { + .umb-media-grid__item-name { + text-decoration: underline; + } + } + } +} + + .umb-media-grid__item.-selected { color:@ui-selected-type; .umb-media-grid__item-overlay { color: @ui-selected-type; } } -.umb-media-grid__item.-selected, +.umb-media-grid__item.-selected, .umb-media-grid__item.-selectable:hover { &::before { content: ""; @@ -118,6 +138,7 @@ .umb-media-grid__item-overlay { display: flex; + width: 100%; opacity: 0; position: absolute; right: 0; @@ -134,9 +155,13 @@ background: fade(@white, 92%); transition: opacity 150ms; - &:hover { + &.-can-open:hover { text-decoration: underline; } + + .tabbing-active &:focus { + opacity: 1; + } } .umb-media-grid__info { @@ -181,7 +206,7 @@ align-items: center; color: @black; transition: opacity 150ms; - + &:hover { color: @ui-action-discreet-type-hover; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-list-view.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-list-view.less index 7a2939bd30..8cd08f5045 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-list-view.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-list-view.less @@ -13,9 +13,15 @@ margin-right: 5px; } +.umb-mini-list-view__breadcrumb { + .flex; + margin-bottom: 10px; + min-height: 25px; +} + .umb-mini-list-view__back { - font-size: 12px; - margin-right: 5px; + font-size: 13px; + margin-right: 5px; color: @gray-4; display: flex; align-items: center; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-search.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-search.less new file mode 100644 index 0000000000..ac15b3dcf8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-search.less @@ -0,0 +1,44 @@ +.umb-mini-search { + position: relative; + display: block; + + .icon { + position: absolute; + padding: 5px 8px; + pointer-events: none; + top: 2px; + color: @ui-action-discreet-type; + transition: color .1s linear; + } + + input { + width: 0px; + padding-left:24px; + margin-bottom: 0px; + background-color: transparent; + border-color: @ui-action-discreet-border; + transition: background-color .1s linear, border-color .1s linear, color .1s linear, width .1s ease-in-out, padding-left .1s ease-in-out; + } + + &:focus-within, &:hover { + .icon { + color: @ui-action-discreet-type-hover; + } + input { + color: @ui-action-discreet-border-hover; + border-color: @ui-action-discreet-border-hover; + } + } + + input:focus, &:focus-within input { + background-color: white; + color: @ui-action-discreet-border-hover; + border-color: @ui-action-discreet-border-hover; + } + + input:focus, &:focus-within input, &.--has-value input { + width: 190px; + padding-left:30px; + } + +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index 699496f5d3..4168ab3c39 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -41,11 +41,8 @@ } .umb-nested-content__item.ui-sortable-placeholder { - background: @gray-10; - border: 1px solid @gray-9; + margin-top: 1px; visibility: visible !important; - height: 55px; - margin-top: -1px; } .umb-nested-content__item--single > .umb-nested-content__content { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less index 16457787a3..b808e6574a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less @@ -67,6 +67,7 @@ height: 100%; width: 100%; border-radius: 3px; + border: 0 none; text-decoration: none !important; transition: border-color 100ms ease; background-color: @white; @@ -193,9 +194,8 @@ color: @black; box-sizing: border-box; justify-content: center; - border-top: 1px solid @gray-8; - border-bottom: 1px solid @gray-8; - border-right: 1px solid @gray-8; + border: 1px solid @gray-8; + border-left: 0; padding: 10px 0; background: @white; } @@ -411,6 +411,7 @@ } .umb-gallery__thumbnail { + background: transparent; flex: 0 1 100px; border: 1px solid @ui-action-discreet-border; border-radius: 3px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-progress-circle.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-progress-circle.less index 03816637a7..d8fb3b4d8f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-progress-circle.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-progress-circle.less @@ -5,6 +5,7 @@ .umb-progress-circle__view-box { position: absolute; transform: rotate(-90deg); + right: 0; } // circle highlight on progressbar diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-sub-views.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-sub-views.less index d3ce368356..cc6be8fa37 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-sub-views.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-sub-views.less @@ -8,7 +8,6 @@ padding-left: 0; padding-right: 0; &:focus { - outline: none; text-decoration: none; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index 94c0318fca..202c488bb4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -34,8 +34,12 @@ text-decoration: none; padding: 0; margin-left: 1px; + body:not(.tabbing-active) & { + outline: 0; + } } + input.umb-table__input { margin: 0 auto; } @@ -47,6 +51,8 @@ input.umb-table__input { .umb-table-head { font-size: 14px; font-weight: bold; + + color: @ui-disabled-type; } .umb-table-head__link { @@ -68,10 +74,12 @@ input.umb-table__input { .umb-table-head__link.sortable { cursor: pointer; + color: @ui-action-discreet-type; &:hover { - text-decoration: none; - color: @black; + color: @ui-action-discreet-type-hover; } + + outline-offset: 1px; } .umb-table-thead__icon { @@ -129,6 +137,9 @@ input.umb-table__input { &::before { opacity:.66; } + .umb-table-body__checkicon { + color: @ui-selected-border; + } } } @@ -141,21 +152,19 @@ input.umb-table__input { } .umb-table-body__link { + position: relative; color: @ui-option-type; font-size: 14px; font-weight: bold; text-decoration: none; - &:hover, &:focus { + &:hover { color: @ui-option-type-hover; text-decoration: underline; - outline: none; } } -.umb-table-body__icon, -.umb-table-body__icon[class^="icon-"], -.umb-table-body__icon[class*=" icon-"] { +.umb-table-body__icon { margin: 0 auto; font-size: 20px; line-height: 20px; @@ -164,13 +173,11 @@ input.umb-table__input { text-decoration: none; } -.umb-table-body__checkicon, -.umb-table-body__checkicon[class^="icon-"], -.umb-table-body__checkicon[class*=" icon-"] { +.umb-table-body__checkicon { display: none; font-size: 18px; line-height: 20px; - color: @green; + color: @ui-selected-border; } .umb-table-body .umb-table__name { @@ -179,7 +186,8 @@ input.umb-table__input { font-weight: bold; a { color: @ui-option-type; - &:hover, &:focus { + outline-offset: 1px; + &:hover { color: @ui-option-type-hover; text-decoration: underline; } @@ -249,8 +257,8 @@ input.umb-table__input { flex-flow: row nowrap; flex: 1 1 5%; position: relative; - margin: auto 14px; - padding: 6px 2px; + margin: auto 0; + padding: 6px 16px; text-align: left; overflow:hidden; } @@ -268,8 +276,8 @@ input.umb-table__input { .umb-table-cell:first-of-type:not(.not-fixed) { flex: 0 0 25px; - margin: 0 0 0 15px; - padding: 15px 0; + margin: 0; + padding: 15px 0 15px 15px; } .umb-table-cell--auto-width { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-picker-list.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-picker-list.less index dff78ce627..0a06120b11 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-picker-list.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-picker-list.less @@ -7,11 +7,17 @@ display: flex; margin-bottom: 5px; padding: 10px; + position: relative; } -.umb-user-group-picker-list-item:active, -.umb-user-group-picker-list-item:focus { - text-decoration: none; +.umb-user-group-picker__action{ + background: transparent; + border: 0 none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } .umb-user-group-picker-list-item:hover { @@ -35,4 +41,4 @@ .umb-user-group-picker-list-item__permission { font-size: 13px; color: @gray-4; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 72abb3ba00..0600c9aab6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -252,7 +252,7 @@ input[type="color"], outline: 0; .tabbing-active & { - outline: 2px solid @inputBorderTabFocus; + outline: 2px solid @ui-outline; } } } @@ -297,11 +297,11 @@ select[size] { } // Focus for select, file, radio, and checkbox -select:focus, -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus { - .tab-focus(); +select, +input[type="file"], +input[type="radio"], +input[type="checkbox"] { + .umb-outline(); } diff --git a/src/Umbraco.Web.UI.Client/src/less/hacks.less b/src/Umbraco.Web.UI.Client/src/less/hacks.less index 3ead4d6905..8d3117febe 100644 --- a/src/Umbraco.Web.UI.Client/src/less/hacks.less +++ b/src/Umbraco.Web.UI.Client/src/less/hacks.less @@ -185,40 +185,38 @@ iframe, .content-column-body { // Inline code // 1: Revert border radius to match look and feel of 7.4+ -code{ - .border-radius(@baseBorderRadius); // 1 +code { + .border-radius(@baseBorderRadius); // 1 } // Blocks of code // 1: Wrapping code is unreadable on small devices. pre { - display: block; - padding: (@baseLineHeight - 1) / 2; - margin: 0 0 @baseLineHeight / 2; - font-family: @sansFontFamily; - //font-size: @baseFontSize - 1; // 14px to 13px - color: @gray-2; - line-height: @baseLineHeight; - white-space: pre-wrap; // 1 - overflow-x: auto; // 1 - background-color: @gray-10; - border: 1px solid @gray-8; - .border-radius(@baseBorderRadius); + display: block; + padding: (@baseLineHeight - 1) / 2; + margin: 0 0 @baseLineHeight / 2; + font-family: @sansFontFamily; + color: @gray-2; + line-height: @baseLineHeight; + white-space: pre-wrap; // 1 + overflow-x: auto; // 1 + background-color: @brownGrayLight; + border: 1px solid @gray-8; + .border-radius(@baseBorderRadius); + // Make prettyprint styles more spaced out for readability + &.prettyprint { + margin-bottom: @baseLineHeight; + } - // Make prettyprint styles more spaced out for readability - &.prettyprint { - margin-bottom: @baseLineHeight; - } - - // Account for some code outputs that place code tags in pre tags - code { - padding: 0; - white-space: pre; // 1 - word-wrap: normal; // 1 - background-color: transparent; - border: 0; - } + // Account for some code outputs that place code tags in pre tags + code { + padding: 0; + white-space: pre; // 1 + word-wrap: normal; // 1 + background-color: transparent; + border: 0; + } } /* Styling for content/media sort order dialog */ diff --git a/src/Umbraco.Web.UI.Client/src/less/installer.less b/src/Umbraco.Web.UI.Client/src/less/installer.less index e964ed3c6f..4e24161e59 100644 --- a/src/Umbraco.Web.UI.Client/src/less/installer.less +++ b/src/Umbraco.Web.UI.Client/src/less/installer.less @@ -3,6 +3,7 @@ @import "variables.less"; // Modify this for custom colors, font-sizes, etc @import "colors.less"; @import "mixins.less"; +@import "application/umb-outline.less"; @import "buttons.less"; @import "forms.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/listview.less b/src/Umbraco.Web.UI.Client/src/less/listview.less index 975dbdbd4a..fe8af6dbc4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/listview.less +++ b/src/Umbraco.Web.UI.Client/src/less/listview.less @@ -1,6 +1,10 @@ // Listview // ------------------------- +.umb-listview { + min-height: 100px; +} + .umb-listview table { border: 1px solid @gray-8; } @@ -43,6 +47,15 @@ /* add padding */ .left-addon input[type="text"] { padding-left: 30px !important; padding-right: 6px; } .right-addon input[type="text"] { padding-right: 30px; padding-left: 6px !important; } + + &__label-icon{ + width: 30px; + height: 30px; + position: absolute; + top: -1px; + left:0; + margin:0 + } } .umb-listview table form { @@ -136,7 +149,36 @@ /* TEMP */ .umb-minilistview { - .umb-table-row.not-allowed { opacity: 0.6; cursor: not-allowed; } + .umb-table-row.not-allowed { + opacity: 0.6; + cursor: not-allowed; + } + + div.umb-mini-list-view__breadcrumb { + margin-bottom: 10px; + } + + div.no-display { + display: none + } + + div.umb-table-cell-padding { + padding-top: 8px; + padding-bottom: 8px; + } + + div.umb-table-cell .form-search { + width: 100%; + margin-right: 0; + + input { + width: 100%; + } + + .icon-search { + font-size: 14px; + } + } } .umb-listview .table-striped tbody td { diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 86a1acbeae..b34f313435 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -117,6 +117,12 @@ h5.-black { } .umb-control-group { position: relative; + + &.umb-control-group__listview { + // position: relative messes up the listview status messages (e.g. "no search results") + position: unset; + } + &::after { content: ''; display:block; @@ -183,6 +189,17 @@ h5.-black { .umb-control-group .umb-el-wrap { padding: 0; } +.umb-control-group .control-header { + + .control-label { + float: left; + } + + .control-description { + display: block; + clear: both; + } +} .form-horizontal .umb-control-group .control-header { float: left; width: 160px; @@ -190,15 +207,12 @@ h5.-black { text-align: left; .control-label { - float: left; width: auto; padding-top: 0; text-align: left; } .control-description { - display: block; - clear: both; max-width:480px;// avoiding description becoming too wide when its placed on top of property. margin-bottom: 10px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index 21b9c5c550..efc0178ca2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/mixins.less +++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less @@ -30,7 +30,6 @@ outline: thin dotted @gray-3; // Webkit outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; } // Center-align a block level element @@ -435,7 +434,7 @@ // Button backgrounds // ------------------ -.buttonBackground(@startColor, @hoverColor: @startColor, @textColor: @white, @textColorHover: @textColor) { +.buttonBackground(@startColor, @hoverColor: @startColor, @textColor: @white, @textColorHover: @textColor, @disabledColor: @sand-1, @disabledTextColor: @white) { color: @textColor; border-color: @startColor @startColor darken(@startColor, 15%); @@ -449,14 +448,14 @@ } // in these cases the gradient won't cover the background, so we override - &:hover, &:focus, &:active, &.active { + &:hover { color: @textColorHover; background-color: @hoverColor; } &.disabled, &[disabled] { - color: @white; - background-color: @sand-1; + background-color: @disabledColor; + color: @disabledTextColor; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/navs.less b/src/Umbraco.Web.UI.Client/src/less/navs.less index 5b97464e31..c347404619 100644 --- a/src/Umbraco.Web.UI.Client/src/less/navs.less +++ b/src/Umbraco.Web.UI.Client/src/less/navs.less @@ -233,11 +233,14 @@ } .dropdown-menu > li > a { + position: relative; padding: 8px 20px; color: @ui-option-type; + text-decoration: none; } .dropdown-menu > li > button { + position: relative; background: transparent; border: 0; padding: 8px 20px; @@ -253,11 +256,9 @@ } .dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus, .dropdown-menu > li > button:hover, -.dropdown-menu > li > button:focus, .dropdown-submenu:hover > a, -.dropdown-submenu:focus > a { +.dropdown-submenu:hover > button { color: @ui-option-type-hover; background: @ui-option-hover; } @@ -300,8 +301,7 @@ // Active:hover/:focus dropdown links // ------------------------- -.nav > .dropdown.active > a:hover, -.nav > .dropdown.active > a:focus { +.nav > .dropdown.active > a:hover { cursor: pointer; } @@ -309,24 +309,21 @@ // ------------------------- .nav-tabs .open .dropdown-toggle, .nav-pills .open .dropdown-toggle, -.nav > li.dropdown.open.active > a:hover, -.nav > li.dropdown.open.active > a:focus { +.nav > li.dropdown.open.active > a:hover { /*color: @white;*/ background-color: @gray-8; border-color: @gray-8; } .nav li.dropdown.open .caret, .nav li.dropdown.open.active .caret, -.nav li.dropdown.open a:hover .caret, -.nav li.dropdown.open a:focus .caret { +.nav li.dropdown.open a:hover .caret { border-top-color: @white; border-bottom-color: @white; .opacity(100); } // Dropdowns in stacked tabs -.tabs-stacked .open > a:hover, -.tabs-stacked .open > a:focus { +.tabs-stacked .open > a:hover { border-color: @gray-8; } @@ -377,15 +374,13 @@ } .tabs-below > .nav-tabs > li > a { .border-radius(0 0 4px 4px); - &:hover, - &:focus { + &:hover { border-bottom-color: transparent; border-top-color: @gray-8; } } .tabs-below > .nav-tabs > .active > a, -.tabs-below > .nav-tabs > .active > a:hover, -.tabs-below > .nav-tabs > .active > a:focus { +.tabs-below > .nav-tabs > .active > a:hover { border-color: transparent @gray-8 @gray-8 @gray-8; } @@ -414,13 +409,11 @@ margin-right: -1px; .border-radius(4px 0 0 4px); } -.tabs-left > .nav-tabs > li > a:hover, -.tabs-left > .nav-tabs > li > a:focus { +.tabs-left > .nav-tabs > li > a:hover { border-color: @gray-10 @gray-8 @gray-10 @gray-10; } .tabs-left > .nav-tabs .active > a, -.tabs-left > .nav-tabs .active > a:hover, -.tabs-left > .nav-tabs .active > a:focus { +.tabs-left > .nav-tabs .active > a:hover { border-color: @gray-8 transparent @gray-8 @gray-8; *border-right-color: @white; } @@ -435,13 +428,11 @@ margin-left: -1px; .border-radius(0 4px 4px 0); } -.tabs-right > .nav-tabs > li > a:hover, -.tabs-right > .nav-tabs > li > a:focus { +.tabs-right > .nav-tabs > li > a:hover { border-color: @gray-10 @gray-10 @gray-10 @gray-8; } .tabs-right > .nav-tabs .active > a, -.tabs-right > .nav-tabs .active > a:hover, -.tabs-right > .nav-tabs .active > a:focus { +.tabs-right > .nav-tabs .active > a:hover { border-color: @gray-8 @gray-8 @gray-8 transparent; *border-left-color: @white; } @@ -456,8 +447,7 @@ color: @gray-8; } // Nuke hover/focus effects -.nav > .disabled > a:hover, -.nav > .disabled > a:focus { +.nav > .disabled > a:hover { text-decoration: none; background-color: transparent; cursor: default; diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index bad0ab9715..40c70f5331 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -341,6 +341,7 @@ .umb-panel-header-icon { cursor: pointer; margin-right: 5px; + margin-top: -6px; height: 50px; display: flex; justify-content: center; diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 8301b6a753..112f94572d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -232,6 +232,7 @@ } .umb-mediapicker-multi > div { width:100%; + .umb-property-editor--limit-width(); } @@ -248,26 +249,11 @@ transition: all 150ms ease-in-out; - &:focus, + &:active, &:hover { color: @ui-action-discreet-type-hover; border-color: @ui-action-discreet-type-hover; } - - &:focus { - outline: none; - .tabbing-active &:after { - content: ''; - position: absolute; - z-index: 10000; - top: -6px; - bottom: -6px; - left: -6px; - right: -6px; - border-radius: 3px; - box-shadow: 0 0 2px @blueMid, inset 0 0 2px 1px @blueMid; - } - } } .umb-mediapicker .label { @@ -880,6 +866,21 @@ .umb-datepicker p {margin-top:10px;} .umb-datepicker p a{color: @gray-3;} +// +// Link picker +// -------------------------------------------------- +.umb-linkpicker { + .umb-linkpicker__url { + width: 50%; + padding-right: 5px; + } + + .umb-linkpicker__anchor { + width: 50%; + padding-left: 5px; + } +} + // // Code mirror - even though this isn't a proprety editor right now, it could be so I'm putting the styles here // -------------------------------------------------- diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 6071c4a5ef..a906bc0eed 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -108,6 +108,7 @@ //@blueLight: #4f89de; @blue: #2E8AEA; @blueMid: #2152A3;// updated 2019 +@blueMidLight: rgb(99, 174, 236); @blueDark: #3544b1;// updated 2019 @blueExtraDark: #1b264f;// added 2019 @blueLight: #ADD8E6; @@ -139,6 +140,9 @@ @ui-option-disabled-type-hover: @gray-5; @ui-option-disabled-hover: @sand-7; +@ui-disabled-type: @gray-6; +@ui-disabled-border: @gray-6; + //@ui-active: #346ab3; @ui-active: @pinkLight; @ui-active-blur: @brownLight; @@ -149,8 +153,8 @@ @ui-selected-hover: ligthen(@sand-5, 10); @ui-selected-type: @blueExtraDark; @ui-selected-type-hover: @blueMid; -@ui-selected-border: @pinkLight; -@ui-selected-border-hover: darken(@pinkLight, 10); +@ui-selected-border: @blueDark; +@ui-selected-border-hover: darken(@blueDark, 10); @ui-light-border: @pinkLight; @ui-light-type: @gray-4; @@ -175,6 +179,8 @@ @ui-action-discreet-border: @gray-7; @ui-action-discreet-border-hover: @blueMid; +@ui-outline: @blueMidLight; + @type-white: @white; @type-black: @blueNight; @@ -255,7 +261,7 @@ // Buttons // ------------------------- @btnBackground: @gray-9; -@btnBackgroundHighlight: @gray-9; +@btnBackgroundHighlight: @gray-10; @btnBorder: @gray-9; @btnPrimaryBackground: @ui-btn-positive; @@ -293,7 +299,7 @@ @inputBackground: @white; @inputBorder: @gray-8; @inputBorderFocus: @gray-7; -@inputBorderTabFocus: @blueExtraDark; +@inputBorderTabFocus: @ui-outline; @inputBorderRadius: 0; @inputDisabledBackground: @gray-10; @formActionsBackground: @gray-9; @@ -448,7 +454,7 @@ @successBorder: transparent; @infoText: @white; -@infoBackground: @turquoise-d1; +@infoBackground: @blueDark; @infoBorder: transparent; @alertBorderRadius: 0; diff --git a/src/Umbraco.Web.UI.Client/src/navigation.controller.js b/src/Umbraco.Web.UI.Client/src/navigation.controller.js index b585d22e9f..194c45afe6 100644 --- a/src/Umbraco.Web.UI.Client/src/navigation.controller.js +++ b/src/Umbraco.Web.UI.Client/src/navigation.controller.js @@ -510,6 +510,14 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar if (!event) { return; } + closeTree(); + }; + + $scope.onOutsideClick = function() { + closeTree(); + }; + + function closeTree() { if (!appState.getGlobalState("touchDevice")) { treeActive = false; $timeout(function () { @@ -518,7 +526,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar } }, 300); } - }; + } $scope.toggleLanguageSelector = function () { $scope.page.languageSelectorIsOpen = !$scope.page.languageSelectorIsOpen; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html index 4ae3121098..96f4a404bc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html @@ -17,17 +17,21 @@
- -
- +
+ -
+
@@ -85,9 +89,9 @@ @@ -96,7 +100,7 @@
- +
Visit umbraco.tv
The best Umbraco video tutorials @@ -104,7 +108,7 @@
- +
Visit our.umbraco.com
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/copy/copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/copy/copy.controller.js index 67604aca44..3b405333bf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/copy/copy.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/copy/copy.controller.js @@ -1,98 +1,99 @@ - (function() { - "use strict"; +(function () { + "use strict"; - function CopyController($scope, localizationService, eventsService, entityHelper) { + function CopyController($scope, localizationService, eventsService, entityHelper) { - var vm = this; + var vm = this; - vm.labels = {}; - vm.hideSearch = hideSearch; - vm.selectResult = selectResult; - vm.onSearchResults = onSearchResults; - vm.submit = submit; - vm.close = close; + vm.labels = {}; + vm.hideSearch = hideSearch; + vm.selectResult = selectResult; + vm.onSearchResults = onSearchResults; + vm.onToggle = toggleHandler; + vm.submit = submit; + vm.close = close; - var dialogOptions = $scope.model; - var node = dialogOptions.currentNode; + var dialogOptions = $scope.model; + var node = dialogOptions.currentNode; - $scope.model.relateToOriginal = true; - $scope.dialogTreeApi = {}; + $scope.model.relateToOriginal = true; + $scope.dialogTreeApi = {}; - vm.searchInfo = { - searchFromId: null, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - }; + vm.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; - // get entity type based on the section - $scope.entityType = entityHelper.getEntityTypeFromSection(dialogOptions.section); + // get entity type based on the section + $scope.entityType = entityHelper.getEntityTypeFromSection(dialogOptions.section); - function onInit() { + function onInit() { - var labelKeys = [ - "general_copy", - "defaultdialogs_relateToOriginalLabel" - ]; + var labelKeys = [ + "general_copy", + "defaultdialogs_relateToOriginalLabel" + ]; - localizationService.localizeMany(labelKeys).then(function (data) { + localizationService.localizeMany(labelKeys).then(function (data) { - vm.labels.title = data[0]; - vm.labels.relateToOriginal = data[1]; + vm.labels.title = data[0]; + vm.labels.relateToOriginal = data[1]; - setTitle(vm.labels.title); - }); - } + setTitle(vm.labels.title); + }); + } - function setTitle(value) { - if (!$scope.model.title) { - $scope.model.title = value; - } - } + function setTitle(value) { + if (!$scope.model.title) { + $scope.model.title = value; + } + } - function nodeSelectHandler(args) { - if (args && args.event) { - args.event.preventDefault(); - args.event.stopPropagation(); - } + function nodeSelectHandler(args) { + if (args && args.event) { + args.event.preventDefault(); + args.event.stopPropagation(); + } - //eventsService.emit("editors.content.copyController.select", args); + //eventsService.emit("editors.content.copyController.select", args); - if ($scope.model.target) { - //un-select if there's a current one selected - $scope.model.target.selected = false; - } + if ($scope.model.target) { + //un-select if there's a current one selected + $scope.model.target.selected = false; + } - $scope.model.target = args.node; - $scope.model.target.selected = true; - } + $scope.model.target = args.node; + $scope.model.target.selected = true; + } - function nodeExpandedHandler(args) { - // open mini list view for list views - if (args.node.metaData.isContainer) { - openMiniListView(args.node); - } - } + function nodeExpandedHandler(args) { + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + } - function hideSearch() { - vm.searchInfo.showSearch = false; - vm.searchInfo.searchFromId = null; - vm.searchInfo.searchFromName = null; - vm.searchInfo.results = []; - } + function hideSearch() { + vm.searchInfo.showSearch = false; + vm.searchInfo.searchFromId = null; + vm.searchInfo.searchFromName = null; + vm.searchInfo.results = []; + } - // method to select a search result - function selectResult(evt, result) { - result.selected = result.selected === true ? false : true; - nodeSelectHandler({ event: evt, node: result }); - } + // method to select a search result + function selectResult(evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler({ event: evt, node: result }); + } - //callback when there are search results - function onSearchResults(results) { - vm.searchInfo.results = results; - vm.searchInfo.showSearch = true; - } + //callback when there are search results + function onSearchResults(results) { + vm.searchInfo.results = results; + vm.searchInfo.showSearch = true; + } $scope.onTreeInit = function () { $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); @@ -100,19 +101,19 @@ } - // Mini list view - $scope.selectListViewNode = function (node) { - node.selected = node.selected === true ? false : true; - nodeSelectHandler({ node: node }); - }; + // Mini list view + $scope.selectListViewNode = function (node) { + node.selected = node.selected === true ? false : true; + nodeSelectHandler({ node: node }); + }; - $scope.closeMiniListView = function () { - $scope.miniListView = undefined; - }; + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; - function openMiniListView(node) { - $scope.miniListView = node; - } + function openMiniListView(node) { + $scope.miniListView = node; + } function submit() { if ($scope.model && $scope.model.submit) { @@ -126,9 +127,16 @@ } } - onInit(); - } + function toggleHandler(type) { + // If the relateToOriginal toggle is clicked + if (type === "relate") { + $scope.model.relateToOriginal = !$scope.model.relateToOriginal; + } + } - angular.module("umbraco").controller("Umbraco.Editors.CopyController", CopyController); + onInit(); + } + + angular.module("umbraco").controller("Umbraco.Editors.CopyController", CopyController); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/copy/copy.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/copy/copy.html index 07c44e90ee..86c0186374 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/copy/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/copy/copy.html @@ -2,7 +2,7 @@ -
+
-
-
- -
-
+ + + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html index 6ba2ec0270..a7d2dbbee2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html @@ -1,4 +1,4 @@ -
+
@@ -16,7 +16,7 @@
- + - +
@@ -159,14 +159,11 @@ -
{{model.result.queryExpression}}
- - copy to clipboard - + {{model.result.queryExpression}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html index e2ae1ab524..b82b919f9f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html @@ -13,10 +13,10 @@ - + - + - + - + - No user groups have been added + No user groups have been added - + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html index 0aab35ca21..c133d5d38b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html index 9a2fe96289..5dd56941a9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html @@ -29,7 +29,7 @@ total-steps="model.steps.length">
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html index 0c4c58c38f..483261a5ad 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html @@ -7,7 +7,12 @@
- + {{vm.buttonLabel}} @@ -18,7 +23,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html index d72e977010..c35686acd1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html @@ -169,7 +169,7 @@ ng-change="updateTemplate(node.template)"> -
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html index 9cd245b0d5..eb12fc7940 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html @@ -27,6 +27,12 @@ + + +
+ This item is in the Recycle Bin +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html index 7430d45ce6..8496aab80c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html @@ -39,10 +39,10 @@ autocomplete="off" maxlength="255" /> - + {{vm.currentVariant.language.name}} @@ -50,10 +50,10 @@ - +
Open in split view
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html index c46efb7b74..e1bc01a7a1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html @@ -12,13 +12,17 @@
-
- -
- -
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html index dda8fa70f4..d743907d07 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html @@ -4,7 +4,7 @@ hotkey="{{::vm.hotkey}}" hotkey-when-hidden="true" ng-class="{'is-active': vm.item.active, '-has-error': vm.item.hasError}" - class="umb-sub-views-nav-item__action"> + class="umb-sub-views-nav-item__action umb-outline umb-outline--thin"> {{ vm.item.name }}
{{vm.item.badge.count}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html index 57a4fdae05..82b21e4c3b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html @@ -16,5 +16,7 @@ - {{vm.text}} + + + {{vm.text}} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/member/umb-member-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/member/umb-member-node-info.html new file mode 100644 index 0000000000..62b2052771 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/member/umb-member-node-info.html @@ -0,0 +1,50 @@ +
+
+ + + +
+
{{ group.label }}
+
+
+ + + +
+
+ +
+ +
+ + + + + + {{node.createDateFormatted}} by {{ node.owner.name }} + + + + {{node.updateDateFormatted}} + + + + + + + + +
{{ node.id }}
+ {{ node.key }} +
+ +
+
+
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html index 46660fc685..ca57679f51 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html @@ -1,17 +1,17 @@
-
+
-
+
{{inheritsFrom}} -
-
@@ -24,7 +24,7 @@ - - - @@ -59,7 +59,7 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html index 33c861c3d0..20718cf804 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html @@ -39,8 +39,8 @@
- Edit - Remove + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html b/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html index ecb1e34e0c..6238d11a69 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html @@ -2,7 +2,7 @@ - +
@@ -24,11 +24,11 @@
Domains
- +
- +
@@ -45,7 +45,9 @@
- + + + @@ -53,9 +55,8 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardIntro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardIntro.html index 0478e6ba3c..17a4dcdb65 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardIntro.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardIntro.html @@ -40,8 +40,12 @@
-

Welcome to The Friendly CMS

-

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

+

+ Welcome to The Friendly CMS +

+

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

diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/members/membersdashboardintro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/members/membersdashboardintro.html deleted file mode 100644 index c29b52d96e..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/members/membersdashboardintro.html +++ /dev/null @@ -1,10 +0,0 @@ -

Start here

- -

Get started with Members right now

-

Use the tool below to search for an existing member.

- -

More about members

- - diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.controller.js index cd936bcdf7..97f8b6bd79 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.controller.js @@ -26,17 +26,16 @@ function ExamineManagementController($scope, $http, $q, $timeout, $location, umb function showSearchResultDialog(values) { if (vm.searchResults) { - localizationService.localize("examineManagement_fieldValues").then(function (value) { - - vm.searchResults.overlay = { + editorService.open({ title: value, searchResultValues: values, + size: "medium", view: "views/dashboard/settings/examinemanagementresults.html", close: function () { - vm.searchResults.overlay = null; + editorService.close(); } - }; + }); }); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.html index 632127e38c..35962be39f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.html @@ -408,10 +408,4 @@
- - - diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagementresults.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagementresults.html index bb8a29fca5..26fa0cb72f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagementresults.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagementresults.html @@ -1,18 +1,41 @@
- - - - - - - - - - - - - - -
FieldValue
{{key}}{{val}}
- + + + + + + + + + + + + + + + + + + + +
FieldValue
{{key}}{{val}}
+
+
+
+ + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html index 4c041a573e..a73b5eccd7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html @@ -5,10 +5,10 @@
-

- The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. - You can add your own health checks, have a look at the documentation for more information about custom health checks. -

+ +

The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. + You can add your own health checks, have a look at the documentation for more information about custom health checks.

+
@@ -85,9 +85,12 @@
{{ vm.selectedGroup.name }}
- +
diff --git a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/modelsbuildermanagement.controller.js similarity index 59% rename from src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.controller.js rename to src/Umbraco.Web.UI.Client/src/views/dashboard/settings/modelsbuildermanagement.controller.js index b0e0c303cf..423a20d864 100644 --- a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/modelsbuildermanagement.controller.js @@ -1,4 +1,4 @@ -function modelsBuilderController($scope, $http, umbRequestHelper, modelsBuilderResource) { +function modelsBuilderManagementController($scope, $http, umbRequestHelper, modelsBuilderManagementResource) { var vm = this; @@ -9,8 +9,8 @@ function generate() { vm.generating = true; umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("modelsBuilderBaseUrl", "BuildModels")), - 'Failed to generate.') + $http.post(umbRequestHelper.getApiUrl("modelsBuilderBaseUrl", "BuildModels")), + 'Failed to generate.') .then(function (result) { vm.generating = false; vm.dashboard = result; @@ -19,7 +19,7 @@ function reload() { vm.loading = true; - modelsBuilderResource.getDashboard().then(function (result) { + modelsBuilderManagementResource.getDashboard().then(function (result) { vm.dashboard = result; vm.loading = false; }); @@ -27,7 +27,7 @@ function init() { vm.loading = true; - modelsBuilderResource.getDashboard().then(function (result) { + modelsBuilderManagementResource.getDashboard().then(function (result) { vm.dashboard = result; vm.loading = false; }); @@ -35,4 +35,4 @@ init(); } -angular.module("umbraco").controller("Umbraco.Dashboard.ModelsBuilderController", modelsBuilderController); \ No newline at end of file +angular.module("umbraco").controller("Umbraco.Dashboard.ModelsBuilderManagementController", modelsBuilderManagementController); diff --git a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/modelsbuildermanagement.html similarity index 94% rename from src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.html rename to src/Umbraco.Web.UI.Client/src/views/dashboard/settings/modelsbuildermanagement.html index 0c10c33e39..1ba86bf72b 100644 --- a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/modelsbuildermanagement.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.html index 6bff0bba9b..95c6af9f79 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.html @@ -27,7 +27,7 @@
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js index 474b07d12c..66983bbc05 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js @@ -10,7 +10,11 @@ function DataTypeEditController($scope, $routeParams, appState, navigationServic var evts = []; var vm = this; - + + vm.header = {}; + vm.header.editorfor = "visuallyHiddenTexts_newDataType"; + vm.header.setPageTitle = true; + //setup scope vars vm.page = {}; vm.page.loading = false; diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html index 0bb2b01e31..2d481de852 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html @@ -8,7 +8,9 @@ hide-icon="true" hide-description="true" hide-alias="true" - navigation="vm.page.navigation"> + navigation="vm.page.navigation" + editorfor="vm.header.editorfor" + setpagetitle="vm.header.setPageTitle"> diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.html index c84eeeedae..e7fdf4f96e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.html @@ -11,7 +11,7 @@
+ on-cancel="cancel" confirm-button-style="danger">
@@ -31,7 +31,7 @@ - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js index 9d73aa8838..b04cccbc0d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js @@ -64,7 +64,8 @@ if (infiniteMode) { documentTypeId = $scope.model.id; create = $scope.model.create; - noTemplate = $scope.model.notemplate; + if (create && !documentTypeId) documentTypeId = -1; + noTemplate = $scope.model.notemplate || $scope.model.noTemplate; isElement = $scope.model.isElement; allowVaryByCulture = $scope.model.allowVaryByCulture; vm.submitButtonKey = "buttons_saveAndClose"; diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/importdocumenttype.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/importdocumenttype.html index 5fd453bcd7..8f84a0cceb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/importdocumenttype.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/importdocumenttype.html @@ -9,7 +9,7 @@ -
-
- - Search - - +
+ diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/delete.html b/src/Umbraco.Web.UI.Client/src/views/macros/delete.html index e6c24507b6..834d7f97ee 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/macros/delete.html @@ -5,6 +5,6 @@ Are you sure you want to delete {{vm.name}}?

- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/edit.html b/src/Umbraco.Web.UI.Client/src/views/macros/edit.html index ddb5f98781..9640419ee2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/macros/edit.html @@ -6,7 +6,8 @@ alias="vm.macro.alias" hide-description="true" hide-icon="true" - navigation="vm.page.navigation"> + navigation="vm.page.navigation"editorfor="vm.header.editorfor" + setpagetitle="vm.header.setPageTitle"> diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js index 272e5dd03a..3261739d36 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js @@ -11,7 +11,10 @@ function MacrosEditController($scope, $q, $routeParams, macroResource, editorSta var vm = this; vm.promises = {}; - + vm.header = {}; + vm.header.editorfor = "general_macro"; + vm.header.setPageTitle = true; + vm.page = {}; vm.page.loading = false; vm.page.saveButtonState = "init"; diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/views/parameters.html b/src/Umbraco.Web.UI.Client/src/views/macros/views/parameters.html index 6ea79fe16d..ccdb41384f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/views/parameters.html +++ b/src/Umbraco.Web.UI.Client/src/views/macros/views/parameters.html @@ -10,13 +10,13 @@
- +
{{parameter.label}} ({{parameter.key}}){{parameter.editor}}
- Edit - Remove + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/views/settings.html b/src/Umbraco.Web.UI.Client/src/views/macros/views/settings.html index d34cf1810d..7706734327 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/views/settings.html +++ b/src/Umbraco.Web.UI.Client/src/views/macros/views/settings.html @@ -11,7 +11,7 @@ - + + - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.controller.js b/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.controller.js new file mode 100644 index 0000000000..635c536816 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.controller.js @@ -0,0 +1,11 @@ +(function () { + "use strict"; + + function MemberAppContentController($scope) { + + var vm = this; + + } + + angular.module("umbraco").controller("Umbraco.Editors.Member.Apps.ContentController", MemberAppContentController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.html b/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.html new file mode 100644 index 0000000000..29df5ba638 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.html @@ -0,0 +1,17 @@ +
+ +
+ +
+
{{ group.label }}
+
+ +
+ + + +
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/member/apps/info/info.html b/src/Umbraco.Web.UI.Client/src/views/member/apps/info/info.html new file mode 100644 index 0000000000..bd9cb98b64 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/member/apps/info/info.html @@ -0,0 +1,4 @@ + + diff --git a/src/Umbraco.Web.UI.Client/src/views/member/edit.html b/src/Umbraco.Web.UI.Client/src/views/member/edit.html index 58bebe1e34..3fec222350 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/edit.html @@ -14,29 +14,22 @@ hide-icon="true" hide-description="true" hide-alias="true" + navigation="content.apps" + on-select-navigation-item="appChanged(item)" show-back-button="showBack()" on-back="onBack()" editorfor="header.editorfor" setpagetitle="header.setPageTitle"> - - -
- -
-
{{ group.label }}
+ + +
+
+
- -
- - - -
-
- -
+ diff --git a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js index 23d35b6925..3ec76deb8e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js @@ -45,9 +45,7 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR $scope.content = data; - setHeaderNameState($scope.content); - - editorState.set($scope.content); + init(); $scope.page.loading = false; @@ -59,9 +57,7 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR .then(function (data) { $scope.content = data; - setHeaderNameState($scope.content); - - editorState.set($scope.content); + init(); $scope.page.loading = false; @@ -90,9 +86,7 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR $scope.content = data; - setHeaderNameState($scope.content); - - editorState.set($scope.content); + init(); if (!infiniteMode) { var path = buildTreePath(data); @@ -121,11 +115,48 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR } - function setHeaderNameState(content) { + function init() { + + var content = $scope.content; + + // we need to check wether an app is present in the current data, if not we will present the default app. + var isAppPresent = false; + + // on first init, we dont have any apps. but if we are re-initializing, we do, but ... + if ($scope.app) { + + // lets check if it still exists as part of our apps array. (if not we have made a change to our docType, even just a re-save of the docType it will turn into new Apps.) + _.forEach(content.apps, function (app) { + if (app === $scope.app) { + isAppPresent = true; + } + }); + + // if we did reload our DocType, but still have the same app we will try to find it by the alias. + if (isAppPresent === false) { + _.forEach(content.apps, function (app) { + if (app.alias === $scope.app.alias) { + isAppPresent = true; + app.active = true; + $scope.appChanged(app); + } + }); + } + + } + + // if we still dont have a app, lets show the first one: + if (isAppPresent === false) { + content.apps[0].active = true; + $scope.appChanged(content.apps[0]); + } + + if (content.membershipScenario === 0) { + $scope.page.nameLocked = true; + } + + editorState.set($scope.content); - if(content.membershipScenario === 0) { - $scope.page.nameLocked = true; - } } /** Just shows a simple notification that there are client side validation issues to be fixed */ @@ -194,6 +225,15 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR }; + $scope.appChanged = function (app) { + $scope.app = app; + + // setup infinite mode + if (infiniteMode) { + $scope.page.submitButtonLabelKey = "buttons_saveAndClose"; + } + } + $scope.showBack = function () { return !!listName; } diff --git a/src/Umbraco.Web.UI.Client/src/views/membergroups/delete.html b/src/Umbraco.Web.UI.Client/src/views/membergroups/delete.html index fff0348b87..17a71fa9de 100644 --- a/src/Umbraco.Web.UI.Client/src/views/membergroups/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/membergroups/delete.html @@ -5,7 +5,7 @@ Are you sure you want to delete {{currentNode.name}}?

- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/delete.html b/src/Umbraco.Web.UI.Client/src/views/membertypes/delete.html index 84eeaa0182..d87b6737f4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/membertypes/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/delete.html @@ -16,7 +16,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/installed.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/installed.html index 7da38183b6..eea0ab88e6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/installed.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/installed.html @@ -18,7 +18,7 @@
{{ installedPackage.name }}
- {{ installedPackage.version }} | {{ installedPackage.url }}| {{ installedPackage.author }} + {{ installedPackage.version }} | {{ installedPackage.url }} | {{ installedPackage.author }}
diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html index 9568536d5f..14aeaf1b58 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html @@ -7,19 +7,31 @@
- -
{{category.name}}
-
+ {{category.name}} +
@@ -30,7 +42,7 @@ @@ -64,7 +78,7 @@ @@ -140,9 +156,9 @@
@@ -274,7 +290,7 @@
@@ -304,7 +320,7 @@
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/delete.html b/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/delete.html index 0adbc2aaa2..b5d187eb57 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/delete.html @@ -5,7 +5,7 @@ Are you sure you want to delete {{currentNode.name}}?

- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.html b/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.html index fa1baa1eff..4964007fb0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.html @@ -12,7 +12,7 @@ Are you sure you want to delete {{currentNode.name}}?

- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.html index 5321e4845d..d797501d91 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.html @@ -15,7 +15,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html index 87ccf08d62..5d93c59f5e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html @@ -27,7 +27,7 @@
  • -
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.html index bb5e5fe782..f569105083 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.html @@ -9,12 +9,12 @@
    - +
    - Remove +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js index 828763bc1c..f9c8ae8b0e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js @@ -19,15 +19,16 @@ angular.module('umbraco') }; } - if($scope.model.value.id && $scope.model.value.type !== "member"){ - entityResource.getById($scope.model.value.id, entityType()).then(function(item){ + if($scope.model.value.id && $scope.model.value.type !== "member"){ + entityResource.getById($scope.model.value.id, entityType()).then(function(item){ populate(item); - }); + }); + } + else { + $timeout(function () { + treeSourceChanged(); + }, 100); } - - $timeout(function () { - treeSourceChanged(); - }, 100); function entityType() { var ent = "Document"; diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js index ef781c6014..a87377c84b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js @@ -7,7 +7,6 @@ function TreeSourceTypePickerController($scope, contentTypeResource, mediaTypeRe var allItemTypes = null; var currentItemType = null; - var initialLoad = true; function init() { vm.loading = true; @@ -86,13 +85,12 @@ function TreeSourceTypePickerController($scope, contentTypeResource, mediaTypeRe } eventsService.on("treeSourceChanged", function (e, args) { - currentItemType = args.value; // reset the model value if we changed node type (but not on the initial load) - if (!initialLoad) { + if (!!currentItemType && currentItemType !== args.value) { vm.itemTypes = []; updateModel(); } - initialLoad = false; + currentItemType = args.value; init(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html index 1056b45f9d..af2dba1d7b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html @@ -21,7 +21,7 @@
    - Remove +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowdeleteconfirm.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowdeleteconfirm.html index 2bf1f00b0e..02b92f44f7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowdeleteconfirm.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowdeleteconfirm.html @@ -1,53 +1,45 @@
    + + + + +

    Warning!

    - +

    + You are deleting the row configuration {{model.dialogData.rowName}} +

    - - - +

    + + Modifying a row configuration name will result in loss of + data for any existing content that is based on this configuration. + +

    +

    + Are you sure? +

    +
    +
    +
    -

    Warning!

    - -

    - You are deleting the row configuration '{{model.dialogData.rowName}}' -

    - -

    - Modifying a row configuration name will result in loss of - data for any existing content that is based on this configuration. -

    - -

    - Are you sure? -

    - - - - -
    -
    -
    - - - - + + - - + - - - + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html index a0faefe690..3164a6964c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html @@ -41,7 +41,8 @@ on-value-changed="focalPointChanged(left, top)" on-image-loaded="imageLoaded(isCroppable, hasDimensions)"> - Remove file + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html index 0aa4b5e561..742f5bafa4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html @@ -53,7 +53,7 @@
    - Cancel +
    @@ -65,8 +65,8 @@

    {{item.alias}} ({{item.width}}px × {{item.height}}px)

    - Edit - Remove + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html index ba08aa2293..c6675ccec8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html @@ -12,7 +12,7 @@
    - +
    @@ -31,7 +31,7 @@
    - (system field) + (system field)
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html index 6c7a9c7f06..07d5215793 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html @@ -52,7 +52,9 @@ + on-click-name="vm.goToItem" + allow-open-folder="true" + allow-open-file="true">
    -
    - - +
  • -
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js index 8381a53644..6b7d9dd7ae 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js @@ -19,7 +19,10 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en var currentForm = angularHelper.getCurrentForm($scope); $scope.sortableOptions = { + axis: "y", + containment: "parent", distance: 10, + opacity: 0.7, tolerance: "pointer", scroll: true, zIndex: 6000, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 635a80dbe9..b61f8b94c2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -55,8 +55,8 @@ }); function setCurrentNode(node) { - vm.currentNode = node; updateModel(); + vm.currentNode = node; } var copyAllEntries = function() { @@ -134,7 +134,7 @@ }; vm.openNodeTypePicker = function ($event) { - if (vm.nodes.length >= vm.maxItems) { + if (vm.overlayMenu || vm.nodes.length >= vm.maxItems) { return; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.controller.js index 4a9a07428d..d4380d6c64 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.controller.js @@ -53,10 +53,6 @@ }); } - $scope.canAdd = function () { - return !$scope.model.docTypes || !$scope.model.value || $scope.model.value.length < $scope.model.docTypes.length; - } - $scope.remove = function (index) { $scope.model.value.splice(index, 1); } @@ -112,6 +108,7 @@ }); }); } + $scope.canAdd = function () { return !$scope.model.value || _.some($scope.model.elemTypes, function (elType) { return !_.find($scope.model.value, function (c) { @@ -120,7 +117,6 @@ }); } - $scope.openElemTypeModal = function ($event, config) { //we have to add the alias to the objects (they are stored as ncAlias) @@ -138,6 +134,9 @@ event: $event, submit: function (model) { config.ncAlias = model.selectedItem.alias; + if (model.selectedItem.tabs.length === 1) { + config.ncTabAlias = model.selectedItem.tabs[0]; + } overlayService.close(); }, close: function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index d24d3796f3..b3821cff3d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -1,6 +1,6 @@ 
    - + @@ -40,8 +40,8 @@
    - diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html index b147e5ccca..d0b5823f74 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html @@ -3,6 +3,6 @@
    -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html index 72b6fb7279..ee2929e3ea 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html @@ -2,16 +2,12 @@
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js index 884cc62d43..4a7fff99f8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js @@ -15,7 +15,22 @@ function textAreaController($scope, validationMessageService) { if ($scope.model.config && $scope.model.config.maxChars) { $scope.model.maxlength = true; } - + + $scope.$on("formSubmitting", function() { + if ($scope.isLengthValid()) { + $scope.textareaFieldForm.textarea.$setValidity("maxChars", true); + } else { + $scope.textareaFieldForm.textarea.$setValidity("maxChars", false); + } + }); + + $scope.isLengthValid = function() { + if (!$scope.model.maxlength) { + return true; + } + return $scope.model.config.maxChars >= $scope.model.count; + } + $scope.model.change = function () { if ($scope.model.value) { $scope.model.count = $scope.model.value.length; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html index 04bd8590d2..d255c4a5d6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html @@ -10,7 +10,7 @@
    %0% characters left.
    -
    +
    Maximum %0% characters, %1% too many.
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js index e86d8caef4..b47c3584b3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js @@ -11,7 +11,19 @@ function textboxController($scope, validationMessageService) { // if no max is specified in the config $scope.model.config.maxChars = 500; } - + + $scope.$on("formSubmitting", function() { + if ($scope.isLengthValid()) { + $scope.textboxFieldForm.textbox.$setValidity("maxChars", true); + } else { + $scope.textboxFieldForm.textbox.$setValidity("maxChars", false); + } + }); + + $scope.isLengthValid = function() { + return $scope.model.config.maxChars >= $scope.model.count; + } + $scope.model.change = function () { if ($scope.model.value) { $scope.model.count = $scope.model.value.length; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html index e1f5dac733..5d86259e93 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html @@ -19,7 +19,7 @@

    {{model.label}} %0% characters left.

    %0% characters left.

    -
    +

    {{model.label}} Maximum %0% characters, %1% too many.

    diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/delete.html b/src/Umbraco.Web.UI.Client/src/views/relationtypes/delete.html index b937d0869d..246e5bdb7a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationtypes/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/delete.html @@ -5,7 +5,7 @@ Are you sure you want to delete {{currentNode.name}}?

    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js index 44fbf6ffe9..74b3e31b87 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js @@ -10,6 +10,10 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource, var vm = this; + vm.header = {}; + vm.header.editorfor = "relationType_tabRelationType"; + vm.header.setPageTitle = true; + vm.page = {}; vm.page.loading = false; vm.page.saveButtonState = "init"; diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.html index 2c86161bda..35e7aa5176 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.html @@ -8,7 +8,9 @@ alias="vm.relationType.alias" hide-description="true" hide-icon="true" - navigation="vm.page.navigation"> + navigation="vm.page.navigation" + editorfor="vm.header.editorfor" + setpagetitle="vm.header.setPageTitle"> diff --git a/src/Umbraco.Web.UI.Client/src/views/scripts/delete.html b/src/Umbraco.Web.UI.Client/src/views/scripts/delete.html index aae686d5ce..c8f6a2a9b5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/scripts/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/scripts/delete.html @@ -5,7 +5,7 @@ Are you sure you want to delete {{currentNode.name}}?

    - + diff --git a/src/Umbraco.Web.UI.Client/src/views/stylesheets/delete.html b/src/Umbraco.Web.UI.Client/src/views/stylesheets/delete.html index d32a7197e1..abd082d413 100644 --- a/src/Umbraco.Web.UI.Client/src/views/stylesheets/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/stylesheets/delete.html @@ -5,7 +5,7 @@ Are you sure you want to delete {{currentNode.name}}?

    - + diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/delete.html b/src/Umbraco.Web.UI.Client/src/views/templates/delete.html index 7c3af5937e..d45353db26 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/templates/delete.html @@ -12,7 +12,7 @@ Are you sure you want to delete {{currentNode.name}}?

    - + diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js index b21859f5c4..984866dca1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js @@ -15,6 +15,8 @@ vm.selectUserGroup = selectUserGroup; vm.deleteUserGroups = deleteUserGroups; + vm.filter = null; + var currentUser = null; function onInit() { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index 4d252a3ae0..46923dd27d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -8,7 +8,7 @@ @@ -18,18 +18,10 @@ - + + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html index ee77e4c14e..a1dcafd421 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html @@ -196,23 +196,22 @@ @@ -249,7 +248,7 @@
    0 && filterCollection && filterCollection.length > 0) { + for (var i = 0; i < selectedCollection.length; i++) { + for (var j = 0; j < filterCollection.length; j++) { + if (filterCollection[j][keyField] === selectedCollection[i]) { + filterCollection[j].selected = true; + } + } + } + } + } + function getSortLabel(sortKey, sortDirection) { var found = _.find(vm.userSortData, function (i) { @@ -467,6 +511,7 @@ vm.usersOptions.userStates.splice(index, 1); } + updateLocation("userStates", vm.usersOptions.userStates.join(",")); getUsers(); } @@ -483,20 +528,28 @@ vm.usersOptions.userGroups.splice(index, 1); } + updateLocation("userGroups", vm.usersOptions.userGroups.join(",")); getUsers(); } function setOrderByFilter(value, direction) { vm.usersOptions.orderBy = value; vm.usersOptions.orderDirection = direction; + updateLocation("orderBy", value); + updateLocation("orderDirection", direction); getUsers(); } function changePageNumber(pageNumber) { vm.usersOptions.pageNumber = pageNumber; + updateLocation("pageNumber", pageNumber); getUsers(); } + function updateLocation(key, value) { + $location.search(key, value); + } + function createUser(addUserForm) { if (formHelper.submitForm({ formCtrl: addUserForm, scope: $scope })) { @@ -575,17 +628,53 @@ } function goToUser(user) { - $location.path(pathToUser(user)).search("create", null).search("invite", null); + $location.path(pathToUser(user)) + .search("orderBy", vm.usersOptions.orderBy) + .search("orderDirection", vm.usersOptions.orderDirection) + .search("pageNumber", vm.usersOptions.pageNumber) + .search("userStates", getUsersOptionsFilterCollectionAsDelimitedStringOrNull(vm.usersOptions.userStates)) + .search("userGroups", getUsersOptionsFilterCollectionAsDelimitedStringOrNull(vm.usersOptions.userGroups)) + .search("create", null) + .search("invite", null); + } + + function getUsersOptionsFilterCollectionAsDelimitedStringOrNull(collection) { + if (collection && collection.length > 0) { + return collection.join(","); + } + + return null; } function getEditPath(user) { - return pathToUser(user) + "?mculture=" + $location.search().mculture; + return pathToUser(user) + usersOptionsAsQueryString(); } - + function pathToUser(user) { return "/users/users/user/" + user.id; } + function usersOptionsAsQueryString() { + var qs = "?orderBy=" + vm.usersOptions.orderBy + + "&orderDirection=" + vm.usersOptions.orderDirection + + "&pageNumber=" + vm.usersOptions.pageNumber; + + qs += addUsersOptionsFilterCollectionToQueryString("userStates", vm.usersOptions.userStates); + qs += addUsersOptionsFilterCollectionToQueryString("userGroups", vm.usersOptions.userGroups); + + qs += "&mculture=" + $location.search().mculture; + + return qs; + } + + function addUsersOptionsFilterCollectionToQueryString(name, collection) { + if (collection && collection.length > 0) { + return "&" + name + "=" + collection.join(","); + } + + return ""; + } + // helpers function getUsers() { @@ -604,6 +693,7 @@ formatDates(vm.users); setUserDisplayState(vm.users); vm.userStatesFilter = usersHelper.getUserStatesFilter(data.userStates); + initUserStateSelections(); vm.loading = false; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 1ed148a23f..638e6376c3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -11,7 +11,7 @@ @@ -27,18 +27,10 @@ - + + + + @@ -180,7 +172,7 @@
    -
    +
    {{ user.userDisplayState.name }} @@ -233,7 +225,7 @@
    + class="umb-table-row umb-user-table-row umb-outline umb-outline--surrounding">

    diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/notifications.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/notifications.spec.js index 28f6f80802..071c2158b8 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/notifications.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/notifications.spec.js @@ -14,21 +14,23 @@ describe('notification tests', function () { var not1 = notifications.success("success", "something great happened"); var not2 = notifications.error("error", "something great happened"); var not3 = notifications.warning("warning", "something great happened"); - var not4 = notifications.warning("warning", ""); + var not4 = notifications.warning("warning", ""); + var not5 = notifications.info("info", "something informative happened"); - expect(notifications.getCurrent().length).toBe(4); + expect(notifications.getCurrent().length).toBe(5); //remove at index 0 notifications.remove(0); - expect(notifications.getCurrent().length).toEqual(3); + expect(notifications.getCurrent().length).toEqual(4); expect(notifications.getCurrent()[0].headline).toBe("error: "); expect(notifications.getCurrent()[1].headline).toBe("warning: "); - expect(notifications.getCurrent()[2].headline).toBe("warning"); + expect(notifications.getCurrent()[2].headline).toBe("warning"); + expect(notifications.getCurrent()[3].headline).toBe("info: "); notifications.removeAll(); expect(notifications.getCurrent().length).toEqual(0); }); }); -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Docs/umb-docs.css b/src/Umbraco.Web.UI.Docs/umb-docs.css index eef0fcee39..931c22b255 100644 --- a/src/Umbraco.Web.UI.Docs/umb-docs.css +++ b/src/Umbraco.Web.UI.Docs/umb-docs.css @@ -76,3 +76,12 @@ a:hover { width: 50px; margin-top: 5px; } + +.content .methods code { + border: 1px solid #e1e1e8; + background-color: #f7f7f9; + padding: 0px 3px; + font-size: 85%; + color: #d14; + white-space: nowrap; +} diff --git a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/package.manifest b/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/package.manifest deleted file mode 100644 index 5e64177046..0000000000 --- a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/package.manifest +++ /dev/null @@ -1,18 +0,0 @@ -{ - // array of files we want to inject into the application - "javascript": [ - "~/App_Plugins/ModelsBuilder/modelsbuilder.controller.js", - "~/App_Plugins/ModelsBuilder/modelsbuilder.resource.js" - ], - - // models builder dashboard - "dashboards": [ - { - "alias": "settingsModelsBuilder", - "name": "Models Builder", - "view": "/App_Plugins/ModelsBuilder/modelsbuilder.html", - "sections": [ "settings" ], - "weight": 40 - } - ] -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 1c2d17aff9..ead0cd1592 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -12,7 +12,7 @@ {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} OnBuildSuccess true - 44319 + 44331 enabled disabled false @@ -81,13 +81,14 @@ + - + @@ -110,9 +111,6 @@ runtime; build; native; contentfiles; analyzers all - - 8.1.0 - @@ -130,6 +128,14 @@ Umbraco.Examine {07FBC26B-2927-4A22-8D96-D644C667FECC} + + {52ac0ba8-a60e-4e36-897b-e8b97a54ed1c} + Umbraco.ModelsBuilder.Embedded + + + {fb5676ed-7a69-492c-b802-e7b24144c0fc} + Umbraco.TestData + {651e1350-91b6-44b7-bd60-7207006d7003} Umbraco.Web @@ -152,9 +158,6 @@ True Settings.settings - - - @@ -180,7 +183,6 @@ - ClientDependency.config Designer @@ -355,7 +357,7 @@ True 8600 / - http://localhost:8600/ + http://localhost:8600 False False diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 0deac8b50f..205c7da224 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -620,6 +620,7 @@ Fortryd Celle margen Vælg + Ryd Luk Luk vindue Kommentar @@ -1177,6 +1178,8 @@ Mange hilsner fra Umbraco robotten Bruger gemt Brugertype gemt Brugergruppe gemt + Sprog og domæner gemt + Der opstod en fejl ved at gemme sprog og domæner Fil ikke gemt Filen kunne ikke gemmes. Tjek filrettighederne Fil gemt diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml index 74f987a4cb..e45d97e7bd 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml @@ -100,11 +100,10 @@ Die Domain '%0%' ist bereits zugeordnet Domain '%0%' aktualisiert Domains bearbeiten - - - + + + Vererben Kultur Definiert die Kultureinstellung für untergeordnete Elemente dieses Elements oder vererbt vom übergeordneten Element. Wird auch auf das aktuelle Element angewendet, sofern auf tieferer Ebene keine Domain zugeordnet ist. @@ -156,7 +155,8 @@ Bestätigen - Anzeigen als + Anzeigen als + Inhalt gelöscht Inhalt unveröffentlicht Inhalt unveröffentlicht für Sprache: %0% @@ -293,6 +293,7 @@ Wollen Sie dieses Element wirklich entfernen? Eigenschaft %0% verwendet Editor %1%, welcher nicht von Nested Content unterstützt wird. + Es wurden keine Dokument-(Inhalts-)Typen für diese Eigenschaft definiert. Füge ein weiteres Textfeld hinzu Entferne dieses Textfeld Inhalt-Basis @@ -309,10 +310,13 @@ Veröffentlichte Sprachen Unveröffentlichte Sprachen Unveränderte Sprachen + Diese Sprachen wurden nicht angelegt Bereit zu Veröffentlichen? Bereit zu Sichern? Freigabe anfordern Wählen Sie Datum und Uhrzeit für die Veröffentlichung bzw. deren Rücknahme. + Neues Element anlegen + Aus der Zwischenablage einfügen Erzeuge eine neue Inhaltsvorlage von '%0%' @@ -348,7 +352,11 @@ Geben Sie einen Verzeichnisnamen ein Wählen Sie einen Namen und einen Typ + Die im Inhaltsbaum ausgewählte Seite erlaubt keine Unterseiten. + Bearbeitungsrechte für diesen Dokumenttyp + Das im Strukturbaum ausgewählte Medienelement erlaubt keine untergeordneten Elemente. + Bearbeitungsrechte für diesen Medientyp Dokumenttyp ohne Vorlage Neues Verzeichnis Neuer Datentyp @@ -408,7 +416,6 @@ Link Anker / querystring Name - Hostnamen verwalten Fenster schließen Wollen Sie dies wirklich entfernen Wollen Sie folgendes wirklich deaktivieren @@ -425,11 +432,14 @@ klicken um Macro hinzuzufügen Tabelle einfügen Dies entfernt die Sprache + + Die Kultur-Variante einer Sprache zu ändern ist möglicherweise eine aufwendige Operation und führt zum Erneuern von Inhalts-Zwischenspeicher und Such-Index. Zuletzt bearbeitet Verknüpfung Anker: Wenn lokale Links verwendet werden, füge ein "#" vor den Link ein In einem neuen Fenster öffnen? + Macro Einstellungen Dieses Makro enthält keine einstellbaren Eigenschaften. Einfügen Berechtigungen bearbeiten für @@ -464,6 +474,7 @@ Link wählen Makro wählen Inhalt wählen + Inhaltstyp wählen Medien-Startknoten wählen Mitglied wählen Mitgliedergruppe wählen @@ -637,7 +648,8 @@ Fenster schließen Kommentar bestätigen - Beschneiden + Beschneiden + Seitenverhältnis beibehalten Inhalt Weiter @@ -702,7 +714,8 @@ Ok Öffnen Optionen - An + An + oder Sortieren nach Kennwort @@ -756,7 +769,8 @@ Sortierung abschließen Vorschau Kennwort ändern - nach + nach + Listenansicht Sichern läuft... Aktuelle(s) @@ -844,8 +858,8 @@ Schauen Sie sich die <strong>Video-Lehrgänge</strong> zum Thema Verzeichnisberechtigungen für Umbraco an oder lesen Sie den technischen Artikel. <strong>Die Dateiberechtigungen sind möglicherweise fehlerhaft!</strong>Sie können Umbraco vermutlich ohne Probleme verwenden, werden aber viele Erweiterungspakete können nicht installiert werden. - <strong>Die Dateiberechtigungen sind nicht geeignet!</strong><br /><br /> - Die Dateiberechtigungen müssen angepasst werden. + <strong>Die Dateiberechtigungen sind nicht geeignet!</strong><br /><br /> + Die Dateiberechtigungen müssen angepasst werden. <strong>Die Dateiberechtigungen sind perfekt eingestellt!</strong><br /><br /> Damit ist Umbraco komplett eingerichtet und es können problemlos Erweiterungspakete installiert werden. Verzeichnisprobleme lösen @@ -1301,7 +1315,8 @@ %0% wurde veröffentlicht %0% und die untergeordneten Elemente wurden veröffentlicht %0% und alle untergeordneten Elemente veröffentlichen - + Sichern und Veröffentlichen, um %0% zu veröffentlicht und auf der Website sichtbar zu machen.

    Sie können dieses Element mitsamt seinen untergeordneten Elementen veröffentlichen, indem Sie Unveröffentlichte Unterelemente einschließen markieren. ]]> @@ -1309,13 +1324,14 @@ Sie haben keine freigegeben Farben konfiguriert - + Sie können nur Elemente folgender Typen wählen: %0% Sie haben ein entferntes oder im Papierkorb befindliches Inhaltselement ausgewählt Sie haben entfernte oder im Papierkorb befindliche Inhaltselemente ausgewählt + Element entfernen Sie haben ein entferntes oder im Papierkorb befindliches Medienelement ausgewählt Sie haben entfernte oder im Papierkorb befindliche medienelemente ausgewählt Verworfen @@ -1481,8 +1497,9 @@ Stil bearbeiten Rich text editor Stile - Definiere die Stile, die im 'Rich-Text-Editor' verfügbar sein sollen. - + Definiere die Stile, die im 'Rich-Text-Editor' verfügbar sein sollen. +
    + Definiere die Styles, die im Rich-Text-Editor dieses Stylesheets verfügbar sein sollen. Stylesheet bearbeiten Stylesheet-Regel bearbeiten Bezeichnung im Auswahlmenü des Rich-Text-Editors @@ -1504,21 +1521,22 @@ Einfügen Wählen Sie, was in die Vorlage eingefügt werden soll Wörterbucheintrag einfügen - Ein Wörterbuchelement ist ein Platzhalter für lokalisierbaren Text. - Das macht es einfach mehrsprachige Websites zu gestalten. + Ein Wörterbuchelement ist ein Platzhalter für lokalisierbaren Text. Das macht es einfach mehrsprachige Websites zu gestalten. Makro - Ein Makro ist eine konfigurierbare Komponente, die großartig für wiederverwendbare Teile Ihres Entwurfes sind, - für welche Sie optionale Parameter benötigen, wie z. B. Galerien, Formulare oder Listen. + Ein Makro ist eine konfigurierbare Komponente, die großartig + für wiederverwendbare Teile Ihres Entwurfes sind, + für welche Sie optionale Parameter benötigen, wie z. B. Galerien, Formulare oder Listen. Umbraco-Feld - Zeigt den Wert eines benannten Feldes der aktuellen Seite an, mit der Möglichkeit den Wert zu verändern - oder einen alternativen Ersatzwert zu wählen. + Zeigt den Wert eines benannten Feldes der aktuellen Seite an, mit der Möglichkeit den Wert zu verändern + oder einen alternativen Ersatzwert zu wählen. + Teilansicht (Partial View) - Eine Teilansicht ist eine eigenständige Vorlagen-Datei, die innerhalb einer anderen Vorlage verwendet werden kann. - Sie ist gut geeignet, um "Markup"-Kode wiederzuverwenden oder komplexe Vorlagen in mehrere Dateien aufzuteilen. + Eine Teilansicht ist eine eigenständige Vorlagen-Datei, die innerhalb einer anderen Vorlage verwendet werden kann. + Sie ist gut geeignet, um "Markup"-Kode wiederzuverwenden oder komplexe Vorlagen in mehrere Dateien aufzuteilen. Basisvorlage Keine Basis @@ -1547,8 +1565,8 @@ Bereichsname Bereich ist notwendig - Wenn notwendig, dann muss die untergeordnete Vorlage eine @section Definition gleichen Namens enthalten, - anderfalls tritt ein Fehler auf. + Wenn notwendig, dann muss die untergeordnete Vorlage eine @section Definition gleichen Namens enthalten, + anderfalls tritt ein Fehler auf. Abfrage-Generator zurückgegebene Elemente, in @@ -1589,7 +1607,7 @@ Neues Element Layout auswählen Neue Zeile - Inhalt hinzufügen + Neuer Inhalt Inhalt entfernen Einstellungen anwenden nicht zugelassen]]> @@ -1623,6 +1641,7 @@ Mischungen + Gruppe Sie haben keine Gruppen hinzugefügt Gruppe hinzufügen Übernimm von @@ -1630,18 +1649,23 @@ Notwendige Bezeichnung Listenansicht aktivieren - Konfiguriert die Verwendung einer sortier- und filterbaren Listenansicht der Unterknoten für diesen Dokumenttyp. - Die Unterknoten werden nicht in Baumstruktur angezeigt. + Konfiguriert die Verwendung einer sortier- und filterbaren Listenansicht der Unterknoten für diesen Dokumenttyp. + Die Unterknoten werden nicht in Baumstruktur angezeigt. + Erlaubte Vorlagen - Wählen Sie die Vorlagen, die Editoren für diesen Dokumenttyp wählen dürfen + Wählen Sie die Vorlagen, die Editoren für diesen Dokumenttyp wählen dürfen + Als Wurzelknoten zulassen - Ermöglicht es Editoren diesen Dokumenttyp in der obersten Ebene der Inhalt-Baum-Strukur zu wählen + Ermöglicht es Editoren diesen Dokumenttyp in der obersten Ebene der Inhalt-Baum-Strukur zu wählen + Erlaubte Dokumenttypen für Unterknoten - Erlaubt es Inhalt der angegebenen Typen unterhalb Inhalten dieses Typs anzulegen - Wählen Sie einen Unterknoten + Erlaubt es Inhalt der angegebenen Typen unterhalb Inhalten dieses Typs anzulegen + + Wählen Sie einen Unterknoten + Übernimm Tabs und Eigenschaften vone einem vorhandenen Dokumenttyp. Neue Tabs werden zum vorliegenden Dokumenttyp hinzugefügt oder mit einem gleichnamigen Tab zusammengeführt. Dieser Dokumenttyp wird in einer Mischung verwendet und kann deshalb nicht selbst zusammengemischt werden. Es sind keine Dokumenttypen für eine Mischung vorhanden. @@ -1666,23 +1690,28 @@ und alle Mitglieder, die auf diesem Typ basieren Mitglied kann bearbeiten - Diese Eigenschaft zur Bearbeitung des Mitglieds auf seiner Profileseite freigeben + Diese Eigenschaft zur Bearbeitung des Mitglieds auf seiner Profileseite freigeben + sensibelle Daten - Diese Eigenschaft für Editoren, die keine Berechtigung für sensibelle Daten haben, verbergen + Diese Eigenschaft für Editoren, die keine Berechtigung für sensibelle Daten haben, verbergen + Auf Mitgliedsprofil anzeigen Diesen Eigenschaftswert für die Anzeige auf der Profilseite des Mitglieds zulassen Tab hat keine Sortierung Wo wird diese Mischung verwendet? - Diese Mischung wird aktuell in den Mischungen folgender Dokumenttypen verwendet: + Diese Mischung wird aktuell in den Mischungen folgender Dokumenttypen verwendet: + Kultur basierte Variationen zulassen Editoren erlauben, Inhalt dieses Typs in verschiedenen Sprachen anzulegen Kultur basierte Variationen zulassen Ist ein Elementtyp - + Nested Content vorgesehen, nicht jedoch als Inhalt-Knoten in der Baumstruktur - ]]> + ]]> + Dies kann nicht für Elementtypen verwendet werden @@ -1695,8 +1724,9 @@ Wird ersetzt durch Kein Ersatzsprache - Um mehrsprachigem Inhalt zu ermöglichen durch eine andere Sprache ersetzt zu werden, - falls die angefragte Sprache nicht verfügbar ist, wählen Sie diese Option hier aus. + Um mehrsprachigem Inhalt zu ermöglichen durch eine andere Sprache ersetzt zu werden, + falls die angefragte Sprache nicht verfügbar ist, wählen Sie diese Option hier aus. + Ersatzsprache @@ -1758,7 +1788,7 @@ Felder Einschließlich der Unterseiten - Schnittstelle für externe Editoren Weiteren Benutzer anlegen - Lege neue Benutzer an, um ihnen Zugang zum Umbraco-Back-Office zu geben. - Während des Anlegens eines neuen Benutzer wird ein Kennwort erzeugt, das Sie dem Benutzer mitteilen können. + Lege neue Benutzer an, um ihnen Zugang zum Umbraco-Back-Office zu geben. + Während des Anlegens eines neuen Benutzer wird ein Kennwort erzeugt, das Sie dem Benutzer mitteilen können. Feld für Beschreibung Benutzer endgültig deaktivieren @@ -1854,12 +1884,14 @@ Editor Feld für Textausschnitt Fehlgeschlagene Anmeldeversuche - Bneutzerprofil aufrufen + Benutzerprofil aufrufen Gruppen hinzufügen, um Zugang und Berechtigungen zuzuweisen Weitere Benutzer einladen - Laden Sie neue Benutzer ein, um ihnen Zugang zum Umbraco-Back-Office zu geben. - Eine Einladungs-E-Mail wird an dem Benutzer geschickt. Diese enthält Informationen, wie sich der Benutzer im Umbraco-Back-Office anmelden kann. - Einladungen sind 72 Stunden lang gültig. + + Laden Sie neue Benutzer ein, um ihnen Zugang zum Umbraco-Back-Office zu geben. + Eine Einladungs-E-Mail wird an dem Benutzer geschickt. Diese enthält Informationen, wie sich der Benutzer im Umbraco-Back-Office anmelden kann. + Einladungen sind 72 Stunden lang gültig. + Sprache Bestimmen Sie die Sprache für Menüs und Dialoge Letztes Abmeldedatum @@ -1914,15 +1946,16 @@ Willkommen bei Umbraco! Bedauerlicherweise ist Ihre Einladung verfallen. Bitte kontaktieren Sie Ihren Administrator und bitten Sie ihn, diese erneut zu schicken. Laden Sie ein Foto von sich hoch, um es anderen Benutzern zu erleichtern, sie zu erkennen. Klicken Sie auf den Kreis oben, um Ihr Foto hochzuladen. Autor - Änderung + Änderung + Ihr Profil Ihr Verlauf Sitzung läuft ab in - Invite user - Create user - Send invite - Back to users - Umbraco: Invitation + Benutzer einladen + Benutzer anlegen + Einladung schicken + Zurück zu den Benutzern + Umbraco: Einladung @@ -2064,8 +2097,10 @@ "customErrors" aktuell auf '%0%' gesetzt. Es wird empfohlen, diese vor dem Live-Gang auf '%1%' zu setzen. "customErrors" erfolgreich auf '%0%' gesetzt. "MacroErrors" auf '%0%' gesetzt. - "MacroErrors" sind auf '%0%' gesetzt, - was verhindert, dass einige oder alle Seiten Ihrer Website vollständig geladen werden, falls Fehler in Makros auftreten. Schaltfläche "Beheben" setzt den Wert auf '%1%'. + + "MacroErrors" sind auf '%0%' gesetzt, + was verhindert, dass einige oder alle Seiten Ihrer Website vollständig geladen werden, falls Fehler in Makros auftreten. Schaltfläche "Beheben" setzt den Wert auf '%1%'. + "MacroErrors" sind jetzt auf '%0%' gesetzt. "trySkipIisCustomErrors" ist auf '%0%' gestellt und Sie verwenden IIS-Version '%1%'. "trySkipIisCustomErrors" ist aktuell auf '%0%' gestellt. Für Ihre IIS-Version (%2%) wird empfohlen, diese auf '%1%' zu stellen. @@ -2116,7 +2151,9 @@ Der 'web.config'-Datei wurde eine Einstellung zum Schutz gegen MIME-'Schnüffeln'-Schwachstellen hinzugefügt. Strict-Transport-Security, auch bekannt als HSTS-Header, ist vorhanden.]]> Strict-Transport-Security, auch bekannt als HSTS-Header, ist nicht vorhanden.]]> - Fügt den Header 'Strict-Transport-Security' mit dem Wert 'max-age=10886400; preload' im Abschnitt 'httpProtocol/customHeaders' der 'web.config'-Datei hinzu. Benutzen Sie dies nur, wenn Sie Ihre Domainen für mindestens 18 Wochen unter HTTPS laufen lassen. + + Fügt den Header 'Strict-Transport-Security' mit dem Wert 'max-age=10886400; preload' im Abschnitt 'httpProtocol/customHeaders' der 'web.config'-Datei hinzu. Benutzen Sie dies nur, wenn Sie Ihre Domainen für mindestens 18 Wochen unter HTTPS laufen lassen. + Die HSTS-Header-Einstellung wurde der 'web.config'-Datei hinzugefügt. X-XSS-Protection ist vorhanden.]]> X-XSS-Protection ist nicht vorhanden]]> @@ -2130,11 +2167,13 @@ Der konfigurierte SMTP-Server mit Host '%0%' und Port '%1%' konnte nicht erreicht werden. Bitte überprüfen Sie die SMTP-Einstellungen in der 'web.config'-Datei im Abschnitt 'system.net/mailsettings'. %0% eingestellt.]]> %0% gestellt.]]> - +

    Die Ergebnisse der geplanten Systemzustandsprüfung läuft am %0% um %1% lauten wie folgt:

    %2% - ]]>
    + ]]> +
    Status der Umbraco Systemzustand: %0% @@ -2182,7 +2221,8 @@ Bidirektional Oberknoten Unterknoten - Anzahl + Anzahl + Relationen Angelegt Kommentar @@ -2203,4 +2243,19 @@ Lassen Sie uns beginnen Umbraco Forms installieren + + zurück gehen + Aktives Layout: + Springe zu + Gruppe + bestanden + alarmierend + fehlgeschlagen + Vorschlag + Prüfung bestanden + Prüfung fehlgeschlagen + Back-Office Suche öffnen + Back-Office Hilfe öffnen / schliessen + Ihre Profil-Einstellungen öffnen / schliessen + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 3f9564f8b2..824fc8f25c 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -265,6 +265,8 @@ Last edited Date/time this document was edited Remove file(s) + Click here to remove the image from the media item + Click here to remove the file from the media item Link to document Member of group(s) Not a member of group(s) @@ -301,6 +303,7 @@ Select the date and time to publish and/or unpublish the content item. Create new Paste from clipboard + This item is in the Recycle Bin Create a new Content Template from '%0%' @@ -625,6 +628,7 @@ Cancel Cell margin Choose + Clear Close Close Window Comment @@ -1402,6 +1406,8 @@ To manage your website, simply open the Umbraco back office and start adding con User Saved User type saved User group saved + Cultures and hostnames saved + Error saving cultures and hostnames File not saved file could not be saved. Please check file permissions File saved @@ -1570,6 +1576,11 @@ To manage your website, simply open the Umbraco back office and start adding con Choose extra Choose default are added + Warning + You are deleting the row configuration + + Deleting a row configuration name will result in loss of data for any existing content that is based on this configuration. + Compositions @@ -1970,6 +1981,7 @@ To manage your website, simply open the Umbraco back office and start adding con Newest Oldest Last login + No user groups have been added Validation @@ -2095,6 +2107,14 @@ To manage your website, simply open the Umbraco back office and start adding con %0%.]]>

    Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

    %2%]]>
    Umbraco Health Check Status: %0% + Check All Groups + Check group + + The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. + You can add your own health checks, have a look at
    the documentation for more information about custom health checks.

    + ]]> + Disable URL tracker @@ -2178,6 +2198,10 @@ To manage your website, simply open the Umbraco back office and start adding con Partial View Partial View Macro Member + Data type + Search the redirect dashboard + Search the user group section + Search the users section References @@ -2340,4 +2364,8 @@ To manage your website, simply open the Umbraco back office and start adding con ]]> + + Welcome to The Friendly CMS + Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index e4b88400e4..229f1cb3f2 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -269,6 +269,8 @@ Last edited Date/time this document was edited Remove file(s) + Click here to remove the image from the media item + Click here to remove the file from the media item Link to document Member of group(s) Not a member of group(s) @@ -305,6 +307,7 @@ Select the date and time to publish and/or unpublish the content item. Create new Paste from clipboard + This item is in the Recycle Bin Create a new Content Template from '%0%' @@ -631,6 +634,7 @@ Cancel Cell margin Choose + Clear Close Close Window Comment @@ -1407,6 +1411,8 @@ To manage your website, simply open the Umbraco back office and start adding con User Saved User type saved User group saved + Cultures and hostnames saved + Error saving cultures and hostnames File not saved file could not be saved. Please check file permissions File saved @@ -1584,6 +1590,11 @@ To manage your website, simply open the Umbraco back office and start adding con Choose extra Choose default are added + Warning + You are deleting the row configuration + + Deleting a row configuration name will result in loss of data for any existing content that is based on this configuration. + Compositions @@ -1983,6 +1994,7 @@ To manage your website, simply open the Umbraco back office and start adding con Newest Oldest Last login + No user groups have been added Validation @@ -2110,6 +2122,14 @@ To manage your website, simply open the Umbraco back office and start adding con %0%.]]>

    Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

    %2%]]>
    Umbraco Health Check Status: %0% + Check All Groups + Check group + + The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. + You can add your own health checks, have a look at the documentation for more information about custom health checks.

    + ]]> +
    Disable URL tracker @@ -2194,6 +2214,10 @@ To manage your website, simply open the Umbraco back office and start adding con Partial View Partial View Macro Member + Data type + Search the redirect dashboard + Search the user group section + Search the users section References @@ -2356,4 +2380,8 @@ To manage your website, simply open the Umbraco back office and start adding con ]]> + + Welcome to The Friendly CMS + Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml index 8ddf2fae26..152a40b965 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml @@ -4,6 +4,9 @@ The Umbraco community https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files + + Innehåll + Hantera domännamn Hantera versioner @@ -11,6 +14,7 @@ Ändra dokumenttyp Kopiera Skapa + Skapa grupp Skapa paket Skapa innehållsmall Standardvärde @@ -38,6 +42,12 @@ Avpublicera Uppdatera + + Innehåll + Administration + Struktur + Övrigt + Lägg till nytt domännamn Domännamn @@ -98,6 +108,7 @@ En innehållsmall är fördefinierat innehåll som en redaktör kan välja att använda som grund för att skapa nytt innehåll + Rensa urval Fetstil Minska indrag Infoga formulärfält @@ -121,6 +132,7 @@ Spara och publicera Spara och schemalägg Spara och skicka för godkännande + Schemaläggning Välj Förhandsgranska Förhandsgranskning är avstängt på grund av att det inte finns någon mall tilldelad @@ -295,6 +307,12 @@ Webbplatsen har indexerats Cache för webbplatsen har uppdaterats. Allt publicerat innehåll är nu uppdaterat. Innehåll som inte har publicerats är fortfarande opublicerat. Webbplatsens cache kommer att uppdateras. Allt innehåll som är publicerat kommer att uppdateras. Innehåll som inte är publicerat kommer att förbli opublicerat. + Välj startnod för innehåll + Välj ikon + Välj startnod för media + Välj användargrupper + Välj sektioner + Välj användare Antal kolumner Antal rader Klicka på förhandsgranskningsbilden för att se bilden i full storlek @@ -361,6 +379,7 @@ Avbryt Cellmarginal Välj + Rensa Stäng Stäng fönstret Kommentar @@ -424,6 +443,7 @@ Egenskaper E-postadress för formulärsdata Papperskorg + Din papperskorg är tom Återstående Ta bort Döp om @@ -567,6 +587,7 @@ Klicka för att ladda upp eller klicka här för att välja filer + Drag och släpp dina filer i denna yta Välj sida ovan... @@ -600,6 +621,7 @@ Paketalternativ Paket läsmig Paketvalv + Sök efter paket Bekräfta avinstallation Paketet har avinstallerats Paketet har avinstallerats utan problem @@ -620,6 +642,7 @@ Fyll i ditt lösenord Skriv för att söka... Fyll i ditt lösenord + Välj alias... Skriv för att lägga till taggar (och tryck enter efter varje tagg)... @@ -690,6 +713,7 @@ Media Medlemmar Nyhetsbrev + Paket Inställningar Statistik Översättning @@ -785,7 +809,7 @@ Sidmall - Image + Bild Macro Lägg till Choose layout @@ -865,6 +889,7 @@ Cacha webbläsare + Innehåll Papperskorg Skapade paket Datatyper @@ -901,24 +926,52 @@ Fel vid kontroll av uppdatering. Se trace-stack för mer information. + Åtkomst + Baserat på tilldelade grupper och startnod så har användaren åtkomst till följande noder + Tilldela åtkomst Administratör Kategorifält + Användare skapad Ändra lösenord - Du kan byta ditt lösenord för Umbraco Back Office genom att fylla i nedanstående formulär och klicka på knappen "Ändra lösenord". + Ändra bild Bekräfta det nya lösenordet + Du kan byta ditt lösenord för Umbraco Back Office genom att fylla i nedanstående formulär och klicka på knappen "Ändra lösenord". Innehållskanal + Skapa en till användare + Skapa nya användare för att ge dom åtkomst till Umbraco. När en ny användare skapas kommer ett lösenord genereras som du kan dela med användaren. Skapa användare + Ta bort användare User + Är du säker på att du vill ta bort användarens konto? Fält för beskrivning Avaktivera användare Dokumenttyp Redaktör Fält för utdrag + Misslyckade inloggningsförsök + Gå till användarens profil + Lägg till grupper för att tilldela åtkomst och rättigheter + Bjud in + Bjud in en till användare + Bjud in nya användare för att ge dom åtkomst till Umbraco. Ett e-postmeddelande kommer skikcas till användaren med information om hur man loggar in i Umbraco. Inbjudningar är giltiga i 72 timmar. Språk + Välj de språk som kommer visas i meny och dialoger + Senast utlåst + Senast inloggad + Lösenordet ändrades Login Startnod i mediabiblioteket + Begränsa media sectionen till en specifik startnod + Media startnoder + Begränsa media sectionen till specifika startnoder Sektioner Byt ditt lösenord + har inte blivit utlåst + har inte loggat in ännu Inaktivera tillgång till Umbraco + Lösenordet har inte ändrats + Ingen startnod vald + Inga startnoder valda + Gammalt lösenord Lösenord Ditt lösenord är nu ändrat! Vänligen bekräfta ditt nya lösenord @@ -932,17 +985,36 @@ Du redigerar nu rättigheterna för sidorna: Välj de sidor vars rättigheter du vill redigera Återställ lösenord + Ta bort bild + Standard rättigheter + Granulära rättigheter Sök igenom alla undernoder + Sätt rättigheter för specifika noder + Profil Sessionen går ut + Välj sektioner för användaråtkomst + Alla + Aktiv + Utlåst + Inbjuden + Inaktiv Namn (A-Z) Namn (Z-A) - Startnod i innehåll + Startnod för innehåll + Begränsa sidträdet till en specifik startnod + Startnoder för innehåll + Begränsa sidträdet till specifika startnoder + Användare ändrad Användarens namn + Användarhantering Användarrättigheter Användartyp Användartyper Skribent Din nuvarande historik Din profil + Nyast + Äldst + Senaste login diff --git a/src/Umbraco.Web.UI/Umbraco/js/navigation.controller.js b/src/Umbraco.Web.UI/Umbraco/js/navigation.controller.js index b585d22e9f..194c45afe6 100644 --- a/src/Umbraco.Web.UI/Umbraco/js/navigation.controller.js +++ b/src/Umbraco.Web.UI/Umbraco/js/navigation.controller.js @@ -510,6 +510,14 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar if (!event) { return; } + closeTree(); + }; + + $scope.onOutsideClick = function() { + closeTree(); + }; + + function closeTree() { if (!appState.getGlobalState("touchDevice")) { treeActive = false; $timeout(function () { @@ -518,7 +526,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar } }, 300); } - }; + } $scope.toggleLanguageSelector = function () { $scope.page.languageSelectorIsOpen = !$scope.page.languageSelectorIsOpen; diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml index 393157bcf8..4a915a444b 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml @@ -1,7 +1,10 @@ @model dynamic @using Umbraco.Web.Templates - +@{ + string embedValue = Convert.ToString(Model.value); + embedValue = embedValue.DetectIsJson() ? Model.value.preview : Model.value; +}
    - @Html.Raw(Model.value) + @Html.Raw(embedValue)
    diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config index 00b556fd34..fa998a9856 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config @@ -1,113 +1,245 @@ - - - - - - - - - - - - - 1 - - - - - - - - your@email.here - - - - - Preview modeClick to end

    ]]> - - - - throw - - - ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,swf,xml,xhtml,html,htm,php,htaccess - - - assets/img/login.jpg - - - - - - false - - true - - false - - - - - - - - - - - - + + + + + + + + + + + + + 1 + + + + + + + + your@email.here + + + + + + Preview mode + + … + + + Click to end + +
    + + ]]> + + + + throw + + + ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,swf,xml,xhtml,html,htm,php,htaccess + + + assets/img/login.jpg + + + + + + false + + true + + false + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index a7f6e1e0f1..9e60a9499c 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -38,7 +38,140 @@ - Preview modeClick to end
    ]]> + + Preview mode + + … + + + Click to end + +
    + + ]]> + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index ff42f098f7..0026f23514 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -21,6 +21,11 @@ + + + + + diff --git a/src/Umbraco.Web/Actions/ActionCollection.cs b/src/Umbraco.Web/Actions/ActionCollection.cs index 89ac8a59f4..95d7b02e90 100644 --- a/src/Umbraco.Web/Actions/ActionCollection.cs +++ b/src/Umbraco.Web/Actions/ActionCollection.cs @@ -28,15 +28,5 @@ namespace Umbraco.Web.Actions .WhereNotNull() .ToList(); } - - internal IReadOnlyList FromEntityPermission(EntityPermission entityPermission) - { - var actions = this.ToArray(); // no worry: internally, it's already an array - return entityPermission.AssignedPermissions - .Where(x => x.Length == 1) - .SelectMany(x => actions.Where(y => y.Letter == x[0])) - .WhereNotNull() - .ToList(); - } } } diff --git a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs index d9a6518493..6abad820c9 100644 --- a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; @@ -139,6 +140,14 @@ namespace Umbraco.Web.Cache public class JsonPayload { + [Obsolete("Use the constructor specifying a GUID instead, using this constructor will result in not refreshing all caches")] + public JsonPayload(int id, TreeChangeTypes changeTypes) + { + Id = id; + ChangeTypes = changeTypes; + } + + [JsonConstructor] public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) { Id = id; diff --git a/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs b/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs index b1fc0a9e55..7c4dab0e17 100644 --- a/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs @@ -25,7 +25,6 @@ namespace Umbraco.Web.Cache public static readonly Guid UniqueId = Guid.Parse("3E0F95D8-0BE5-44B8-8394-2B8750B62654"); private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly IDomainService _domainService; public override Guid RefresherUniqueId => UniqueId; diff --git a/src/Umbraco.Web/ContentApps/ContentEditorContentAppFactory.cs b/src/Umbraco.Web/ContentApps/ContentEditorContentAppFactory.cs index 8c251cacd2..add7e2f16a 100644 --- a/src/Umbraco.Web/ContentApps/ContentEditorContentAppFactory.cs +++ b/src/Umbraco.Web/ContentApps/ContentEditorContentAppFactory.cs @@ -14,6 +14,7 @@ namespace Umbraco.Web.ContentApps private ContentApp _contentApp; private ContentApp _mediaApp; + private ContentApp _memberApp; public ContentApp GetContentAppFor(object o, IEnumerable userGroups) { @@ -45,6 +46,16 @@ namespace Umbraco.Web.ContentApps case IMedia _: return null; + case IMember _: + return _memberApp ?? (_memberApp = new ContentApp + { + Alias = "umbContent", + Name = "Content", + Icon = Constants.Icons.Content, + View = "views/member/apps/content/content.html", + Weight = Weight + }); + default: throw new NotSupportedException($"Object type {o.GetType()} is not supported here."); } diff --git a/src/Umbraco.Web/ContentApps/ContentInfoContentAppFactory.cs b/src/Umbraco.Web/ContentApps/ContentInfoContentAppFactory.cs index 49be194349..fac03c43d0 100644 --- a/src/Umbraco.Web/ContentApps/ContentInfoContentAppFactory.cs +++ b/src/Umbraco.Web/ContentApps/ContentInfoContentAppFactory.cs @@ -13,6 +13,7 @@ namespace Umbraco.Web.ContentApps private ContentApp _contentApp; private ContentApp _mediaApp; + private ContentApp _memberApp; public ContentApp GetContentAppFor(object o, IEnumerable userGroups) { @@ -37,6 +38,15 @@ namespace Umbraco.Web.ContentApps View = "views/media/apps/info/info.html", Weight = Weight }); + case IMember _: + return _memberApp ?? (_memberApp = new ContentApp + { + Alias = "umbInfo", + Name = "Info", + Icon = "icon-info", + View = "views/member/apps/info/info.html", + Weight = Weight + }); default: throw new NotSupportedException($"Object type {o.GetType()} is not supported here."); diff --git a/src/Umbraco.Web/ContentApps/ListViewContentAppFactory.cs b/src/Umbraco.Web/ContentApps/ListViewContentAppFactory.cs index ae6d324e84..bf6184197f 100644 --- a/src/Umbraco.Web/ContentApps/ListViewContentAppFactory.cs +++ b/src/Umbraco.Web/ContentApps/ListViewContentAppFactory.cs @@ -45,6 +45,8 @@ namespace Umbraco.Web.ContentApps entityType = "media"; dtdId = Core.Constants.DataTypes.DefaultMediaListView; break; + case IMember member: + return null; default: throw new NotSupportedException($"Object type {o.GetType()} is not supported here."); } diff --git a/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs b/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs index 11a876345d..a6cba6ce41 100644 --- a/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs +++ b/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs @@ -49,18 +49,15 @@ namespace Umbraco.Web.Editors.Binders { foreach (var variant in model.Variants) { - if (variant.Culture.IsNullOrWhiteSpace()) - { - //map the property dto collection (no culture is passed to the mapping context so it will be invariant) - variant.PropertyCollectionDto = Current.Mapper.Map(model.PersistedContent); - } - else - { - //map the property dto collection with the culture of the current variant - variant.PropertyCollectionDto = Current.Mapper.Map( - model.PersistedContent, - context => context.SetCulture(variant.Culture)); - } + //map the property dto collection with the culture of the current variant + variant.PropertyCollectionDto = Current.Mapper.Map( + model.PersistedContent, + context => + { + // either of these may be null and that is ok, if it's invariant they will be null which is what is expected + context.SetCulture(variant.Culture); + context.SetSegment(variant.Segment); + }); //now map all of the saved values to the dto _modelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto); @@ -87,6 +84,5 @@ namespace Umbraco.Web.Editors.Binders model.ParentId, contentType); } - } } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index d19c066861..f5d72894ca 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -303,7 +303,7 @@ namespace Umbraco.Web.Editors } /// - /// Gets the content json for the content id + /// Gets the content json for the content guid /// /// /// @@ -323,7 +323,7 @@ namespace Umbraco.Web.Editors } /// - /// Gets the content json for the content id + /// Gets the content json for the content udi /// /// /// @@ -341,7 +341,7 @@ namespace Umbraco.Web.Editors } /// - /// Gets an empty content item for the + /// Gets an empty content item for the document type. /// /// /// @@ -1679,9 +1679,9 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(response); } - var permission = Services.UserService.GetPermissions(Security.CurrentUser, node.Path); + var assignedPermissions = Services.UserService.GetAssignedPermissions(Security.CurrentUser, node.Id); - if (permission.AssignedPermissions.Contains(ActionAssignDomain.ActionLetter.ToString(), StringComparer.Ordinal) == false) + if (assignedPermissions.Contains(ActionAssignDomain.ActionLetter.ToString(), StringComparer.Ordinal) == false) { var response = Request.CreateResponse(HttpStatusCode.BadRequest); response.Content = new StringContent("You do not have permission to assign domains on that node."); @@ -1836,8 +1836,13 @@ namespace Umbraco.Web.Editors /// private void MapValuesForPersistence(ContentItemSave contentSave) { - // inline method to determine if a property type varies - bool Varies(Property property) => property.PropertyType.VariesByCulture(); + // inline method to determine the culture and segment to persist the property + (string culture, string segment) PropertyCultureAndSegment(Property property, ContentVariantSave variant) + { + var culture = property.PropertyType.VariesByCulture() ? variant.Culture : null; + var segment = property.PropertyType.VariesBySegment() ? variant.Segment : null; + return (culture, segment); + } var variantIndex = 0; @@ -1876,8 +1881,18 @@ namespace Umbraco.Web.Editors MapPropertyValuesForPersistence( contentSave, propertyCollection, - (save, property) => Varies(property) ? property.GetValue(variant.Culture) : property.GetValue(), //get prop val - (save, property, v) => { if (Varies(property)) property.SetValue(v, variant.Culture); else property.SetValue(v); }, //set prop val + (save, property) => + { + // Get property value + (var culture, var segment) = PropertyCultureAndSegment(property, variant); + return property.GetValue(culture, segment); + }, + (save, property, v) => + { + // Set property value + (var culture, var segment) = PropertyCultureAndSegment(property, variant); + property.SetValue(v, culture, segment); + }, variant.Culture); variantIndex++; diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs index 31e0d70a42..5f5f5104cb 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web/Editors/DataTypeController.cs @@ -20,6 +20,7 @@ using Umbraco.Web.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; +using System.Web.Http.Controllers; namespace Umbraco.Web.Editors { @@ -34,6 +35,7 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] [UmbracoTreeAuthorize(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)] [EnableOverrideAuthorization] + [DataTypeControllerConfiguration] public class DataTypeController : BackOfficeNotificationsController { private readonly PropertyEditorCollection _propertyEditors; @@ -44,6 +46,19 @@ namespace Umbraco.Web.Editors _propertyEditors = propertyEditors; } + /// + /// Configures this controller with a custom action selector + /// + private class DataTypeControllerConfigurationAttribute : Attribute, IControllerConfiguration + { + public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) + )); + } + } + /// /// Gets data type by name /// @@ -70,6 +85,40 @@ namespace Umbraco.Web.Editors return Mapper.Map(dataType); } + /// + /// Gets the datatype json for the datatype guid + /// + /// + /// + public DataTypeDisplay GetById(Guid id) + { + var dataType = Services.DataTypeService.GetDataType(id); + if (dataType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + return Mapper.Map(dataType); + } + + /// + /// Gets the datatype json for the datatype udi + /// + /// + /// + public DataTypeDisplay GetById(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + var dataType = Services.DataTypeService.GetDataType(guidUdi.Guid); + if (dataType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + return Mapper.Map(dataType); + } + /// /// Deletes a data type with a given ID /// diff --git a/src/Umbraco.Web/Editors/EditorValidatorCollection.cs b/src/Umbraco.Web/Editors/EditorValidatorCollection.cs index 6fc6bb5de2..0e42b0027c 100644 --- a/src/Umbraco.Web/Editors/EditorValidatorCollection.cs +++ b/src/Umbraco.Web/Editors/EditorValidatorCollection.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Composing; namespace Umbraco.Web.Editors { - internal class EditorValidatorCollection : BuilderCollectionBase + public class EditorValidatorCollection : BuilderCollectionBase { public EditorValidatorCollection(IEnumerable items) : base(items) diff --git a/src/Umbraco.Web/Editors/EditorValidatorCollectionBuilder.cs b/src/Umbraco.Web/Editors/EditorValidatorCollectionBuilder.cs index b3b7bab1a5..994a813cf4 100644 --- a/src/Umbraco.Web/Editors/EditorValidatorCollectionBuilder.cs +++ b/src/Umbraco.Web/Editors/EditorValidatorCollectionBuilder.cs @@ -2,7 +2,7 @@ namespace Umbraco.Web.Editors { - internal class EditorValidatorCollectionBuilder : LazyCollectionBuilderBase + public class EditorValidatorCollectionBuilder : LazyCollectionBuilderBase { protected override EditorValidatorCollectionBuilder This => this; } diff --git a/src/Umbraco.Web/Editors/HelpController.cs b/src/Umbraco.Web/Editors/HelpController.cs index ccbbcaeee8..39dbbc435c 100644 --- a/src/Umbraco.Web/Editors/HelpController.cs +++ b/src/Umbraco.Web/Editors/HelpController.cs @@ -13,14 +13,23 @@ namespace Umbraco.Web.Editors { var url = string.Format(baseUrl + "/Umbraco/Documentation/Lessons/GetContextHelpDocs?sectionAlias={0}&treeAlias={1}", section, tree); - if (_httpClient == null) - _httpClient = new HttpClient(); + try + { - //fetch dashboard json and parse to JObject - var json = await _httpClient.GetStringAsync(url); - var result = JsonConvert.DeserializeObject>(json); - if (result != null) - return result; + if (_httpClient == null) + _httpClient = new HttpClient(); + + //fetch dashboard json and parse to JObject + var json = await _httpClient.GetStringAsync(url); + var result = JsonConvert.DeserializeObject>(json); + if (result != null) + return result; + + } + catch (HttpRequestException rex) + { + Logger.Info(GetType(), $"Check your network connection, exception: {rex.Message}"); + } return new List(); } diff --git a/src/Umbraco.Web/Editors/IEditorValidator.cs b/src/Umbraco.Web/Editors/IEditorValidator.cs index d469d9d9eb..2d655e3506 100644 --- a/src/Umbraco.Web/Editors/IEditorValidator.cs +++ b/src/Umbraco.Web/Editors/IEditorValidator.cs @@ -14,9 +14,7 @@ namespace Umbraco.Web.Editors // initialized with all IEditorValidator instances // // validation is used exclusively in ContentTypeControllerBase - // the whole thing is internal at the moment, never released - // and, there are no IEditorValidator implementation in Core - // so... this all mechanism is basically useless + // currently the only implementations are for Models Builder. /// /// Provides a general object validator. diff --git a/src/Umbraco.Web/Editors/MacrosController.cs b/src/Umbraco.Web/Editors/MacrosController.cs index 3cb161e547..38103400d9 100644 --- a/src/Umbraco.Web/Editors/MacrosController.cs +++ b/src/Umbraco.Web/Editors/MacrosController.cs @@ -19,6 +19,7 @@ using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; +using System.Web.Http.Controllers; namespace Umbraco.Web.Editors { @@ -28,6 +29,7 @@ namespace Umbraco.Web.Editors /// [PluginController("UmbracoApi")] [UmbracoTreeAuthorize(Constants.Trees.Macros)] + [MacrosControllerConfiguration] public class MacrosController : BackOfficeNotificationsController { private readonly IMacroService _macroService; @@ -38,6 +40,19 @@ namespace Umbraco.Web.Editors _macroService = Services.MacroService; } + /// + /// Configures this controller with a custom action selector + /// + private class MacrosControllerConfigurationAttribute : Attribute, IControllerConfiguration + { + public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) + )); + } + } + /// /// Creates a new macro /// @@ -97,39 +112,43 @@ namespace Umbraco.Web.Editors return this.ReturnErrorResponse($"Macro with id {id} does not exist"); } - var macroDisplay = new MacroDisplay - { - Alias = macro.Alias, - Id = macro.Id, - Key = macro.Key, - Name = macro.Name, - CacheByPage = macro.CacheByPage, - CacheByUser = macro.CacheByMember, - CachePeriod = macro.CacheDuration, - View = macro.MacroSource, - RenderInEditor = !macro.DontRender, - UseInEditor = macro.UseInEditor, - Path = $"-1,{macro.Id}" - }; - - var parameters = new List(); - - foreach (var param in macro.Properties.Values.OrderBy(x => x.SortOrder)) - { - parameters.Add(new MacroParameterDisplay - { - Editor = param.EditorAlias, - Key = param.Alias, - Label = param.Name, - Id = param.Id - }); - } - - macroDisplay.Parameters = parameters; + var macroDisplay = MapToDisplay(macro); return this.Request.CreateResponse(HttpStatusCode.OK, macroDisplay); } + [HttpGet] + public HttpResponseMessage GetById(Guid id) + { + var macro = _macroService.GetById(id); + + if (macro == null) + { + return this.ReturnErrorResponse($"Macro with id {id} does not exist"); + } + + var macroDisplay = MapToDisplay(macro); + + return this.Request.CreateResponse(HttpStatusCode.OK, macroDisplay); + } + + [HttpGet] + public HttpResponseMessage GetById(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi == null) + this.ReturnErrorResponse($"Macro with id {id} does not exist"); + + var macro = _macroService.GetById(guidUdi.Guid); + if (macro == null) + { + return this.ReturnErrorResponse($"Macro with id {id} does not exist"); + } + + var macroDisplay = MapToDisplay(macro); + + return this.Request.CreateResponse(HttpStatusCode.OK, macroDisplay); + } [HttpPost] public HttpResponseMessage DeleteById(int id) @@ -323,7 +342,6 @@ namespace Umbraco.Web.Editors /// Finds partial view files in app plugin folders. /// /// - /// The . /// private IEnumerable FindPartialViewFilesInPluginFolders() { @@ -385,5 +403,29 @@ namespace Umbraco.Web.Editors return files; } + + /// + /// Used to map an instance to a + /// + /// + /// + private MacroDisplay MapToDisplay(IMacro macro) + { + var display = Mapper.Map(macro); + + var parameters = macro.Properties.Values + .OrderBy(x => x.SortOrder) + .Select(x => new MacroParameterDisplay() + { + Editor = x.EditorAlias, + Key = x.Alias, + Label = x.Name, + Id = x.Id + }); + + display.Parameters = parameters; + + return display; + } } } diff --git a/src/Umbraco.Web/Editors/TourController.cs b/src/Umbraco.Web/Editors/TourController.cs index 166e5a894d..25b4f7e9fc 100644 --- a/src/Umbraco.Web/Editors/TourController.cs +++ b/src/Umbraco.Web/Editors/TourController.cs @@ -51,29 +51,35 @@ namespace Umbraco.Web.Editors } //collect all tour files in packages - foreach (var plugin in Directory.EnumerateDirectories(IOHelper.MapPath(SystemDirectories.AppPlugins))) + var appPlugins = IOHelper.MapPath(SystemDirectories.AppPlugins); + if (Directory.Exists(appPlugins)) { - var pluginName = Path.GetFileName(plugin.TrimEnd('\\')); - var pluginFilters = _filters.Where(x => x.PluginName != null && x.PluginName.IsMatch(pluginName)).ToList(); - - //If there is any filter applied to match the plugin only (no file or tour alias) then ignore the plugin entirely - var isPluginFiltered = pluginFilters.Any(x => x.TourFileName == null && x.TourAlias == null); - if (isPluginFiltered) continue; - - //combine matched package filters with filters not specific to a package - var combinedFilters = nonPluginFilters.Concat(pluginFilters).ToList(); - - foreach (var backofficeDir in Directory.EnumerateDirectories(plugin, "backoffice")) + foreach (var plugin in Directory.EnumerateDirectories(appPlugins)) { - foreach (var tourDir in Directory.EnumerateDirectories(backofficeDir, "tours")) + var pluginName = Path.GetFileName(plugin.TrimEnd('\\')); + var pluginFilters = _filters.Where(x => x.PluginName != null && x.PluginName.IsMatch(pluginName)) + .ToList(); + + //If there is any filter applied to match the plugin only (no file or tour alias) then ignore the plugin entirely + var isPluginFiltered = pluginFilters.Any(x => x.TourFileName == null && x.TourAlias == null); + if (isPluginFiltered) continue; + + //combine matched package filters with filters not specific to a package + var combinedFilters = nonPluginFilters.Concat(pluginFilters).ToList(); + + foreach (var backofficeDir in Directory.EnumerateDirectories(plugin, "backoffice")) { - foreach (var tourFile in Directory.EnumerateFiles(tourDir, "*.json")) + foreach (var tourDir in Directory.EnumerateDirectories(backofficeDir, "tours")) { - TryParseTourFile(tourFile, result, combinedFilters, aliasOnlyFilters, pluginName); + foreach (var tourFile in Directory.EnumerateFiles(tourDir, "*.json")) + { + TryParseTourFile(tourFile, result, combinedFilters, aliasOnlyFilters, pluginName); + } } } } } + //Get all allowed sections for the current user var allowedSections = user.AllowedSections.ToList(); diff --git a/src/Umbraco.Web/ExamineExtensions.cs b/src/Umbraco.Web/ExamineExtensions.cs index 9a9fa98d95..421993f8fd 100644 --- a/src/Umbraco.Web/ExamineExtensions.cs +++ b/src/Umbraco.Web/ExamineExtensions.cs @@ -2,32 +2,95 @@ using System.Collections.Generic; using System.Linq; using Examine; -using Umbraco.Core; +using Examine.LuceneEngine.Providers; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Examine; using Umbraco.Web.PublishedCache; namespace Umbraco.Web { /// - /// Extension methods for Examine + /// Extension methods for Examine. /// public static class ExamineExtensions { + /// + /// Creates an containing all content from the . + /// + /// The search results. + /// The cache to fetch the content from. + /// + /// An containing all content. + /// + /// cache + /// + /// Search results are skipped if it can't be fetched from the by its integer id. + /// public static IEnumerable ToPublishedSearchResults(this IEnumerable results, IPublishedCache cache) { - var list = new List(); + if (cache == null) throw new ArgumentNullException(nameof(cache)); - foreach (var result in results.OrderByDescending(x => x.Score)) + var publishedSearchResults = new List(); + + foreach (var result in results) { - if (!int.TryParse(result.Id, out var intId)) continue; //invalid - var content = cache.GetById(intId); - if (content == null) continue; // skip if this doesn't exist in the cache - - list.Add(new PublishedSearchResult(content, result.Score)); - + if (int.TryParse(result.Id, out var contentId) && + cache.GetById(contentId) is IPublishedContent content) + { + publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); + } } - return list; + return publishedSearchResults; + } + + /// + /// Creates an containing all content, media or members from the . + /// + /// The search results. + /// The snapshot. + /// + /// An containing all content, media or members. + /// + /// snapshot + /// + /// Search results are skipped if it can't be fetched from the respective cache by its integer id. + /// + public static IEnumerable ToPublishedSearchResults(this IEnumerable results, IPublishedSnapshot snapshot) + { + if (snapshot == null) throw new ArgumentNullException(nameof(snapshot)); + + var publishedSearchResults = new List(); + + foreach (var result in results) + { + if (int.TryParse(result.Id, out var contentId) && + result.Values.TryGetValue(LuceneIndex.CategoryFieldName, out var indexType)) + { + IPublishedContent content; + switch (indexType) + { + case IndexTypes.Content: + content = snapshot.Content.GetById(contentId); + break; + case IndexTypes.Media: + content = snapshot.Media.GetById(contentId); + break; + case IndexTypes.Member: + content = snapshot.Members.GetById(contentId); + break; + default: + continue; + } + + if (content != null) + { + publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); + } + } + } + + return publishedSearchResults; } } } diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs index 0fd591e96b..ebe5e08f89 100644 --- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs @@ -64,7 +64,8 @@ namespace Umbraco.Web var htmlBadge = String.Format(Current.Configs.Settings().Content.PreviewBadge, IOHelper.ResolveUrl(SystemDirectories.Umbraco), - Current.UmbracoContext.HttpContext.Server.UrlEncode(Current.UmbracoContext.HttpContext.Request.Path)); + Current.UmbracoContext.HttpContext.Server.UrlEncode(Current.UmbracoContext.HttpContext.Request.Path), + Current.UmbracoContext.PublishedRequest.PublishedContent.Id); return new MvcHtmlString(htmlBadge); } return new MvcHtmlString(""); diff --git a/src/Umbraco.Web/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index 8a8d678aba..7066475dc9 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -35,52 +35,63 @@ namespace Umbraco.Web /// /// Searches content. /// - /// Term to search. - /// Optional culture. - /// Optional index name. + /// The term to search. + /// The culture (defaults to a culture insensitive search). + /// The name of the index to search (defaults to ). + /// + /// The search results. + /// /// /// - /// When the is not specified or is *, all cultures are searched. + /// When the is not specified or is *, all cultures are searched. /// To search for only invariant documents and fields use null. /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. /// /// While enumerating results, the ambient culture is changed to be the searched culture. /// - IEnumerable Search(string term, string culture = "*", string indexName = null); + IEnumerable Search(string term, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName); /// /// Searches content. /// - /// Term to search. - /// Numbers of items to skip. - /// Numbers of items to return. - /// Total number of matching items. - /// Optional culture. - /// Optional index name. + /// The term to search. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// The culture (defaults to a culture insensitive search). + /// The name of the index to search (defaults to ). + /// + /// The search results. + /// /// /// - /// When the is not specified or is *, all cultures are searched. + /// When the is not specified or is *, all cultures are searched. /// To search for only invariant documents and fields use null. /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. /// /// While enumerating results, the ambient culture is changed to be the searched culture. /// - IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null); + IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName); /// - /// Executes the query and converts the results to PublishedSearchResult. + /// Executes the query and converts the results to . /// - /// - /// While enumerating results, the ambient culture is changed to be the searched culture. - /// + /// The query. + /// + /// The search results. + /// IEnumerable Search(IQueryExecutor query); /// - /// Executes the query and converts the results to PublishedSearchResult. + /// Executes the query and converts the results to . /// - /// - /// While enumerating results, the ambient culture is changed to be the searched culture. - /// + /// The query. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// + /// The search results. + /// IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords); } } diff --git a/src/Umbraco.Web/Install/FilePermissionHelper.cs b/src/Umbraco.Web/Install/FilePermissionHelper.cs index ede9008514..af62b4fd9e 100644 --- a/src/Umbraco.Web/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Web/Install/FilePermissionHelper.cs @@ -45,8 +45,8 @@ namespace Umbraco.Web.Install /// /// This will test the directories for write access /// - /// - /// + /// + /// /// /// If this is false, the easiest way to test for write access is to write a temp file, however some folder will cause /// an App Domain restart if a file is written to the folder, so in that case we need to use the ACL APIs which aren't as diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs index c5c22484ad..2b70a63035 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs @@ -53,11 +53,21 @@ namespace Umbraco.Web.Models.ContentEditing [ReadOnly(true)] public string Culture { get; set; } + /// + /// The segment of the property + /// + /// + /// The segment value of a property can always be null but can only have a non-null value + /// when the property can be varied by segment. + /// + [DataMember(Name = "segment")] + [ReadOnly(true)] + public string Segment { get; set; } + /// /// Used internally during model mapping /// [IgnoreDataMember] internal IDataEditor PropertyEditor { get; set; } - } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentVariantSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentVariantSave.cs index deadac949a..9a7555ad92 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentVariantSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentVariantSave.cs @@ -29,6 +29,12 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "culture")] public string Culture { get; set; } + /// + /// The segment of this variant, if this is invariant than this is null or empty + /// + [DataMember(Name = "segment")] + public string Segment { get; set; } + /// /// Indicates if the variant should be updated /// diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs index fd1c1ed5b8..3857731671 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Core.Models; +using Umbraco.Core.Models.ContentEditing; using Umbraco.Core.Models.Membership; namespace Umbraco.Web.Models.ContentEditing @@ -15,6 +16,7 @@ namespace Umbraco.Web.Models.ContentEditing public MemberDisplay() { MemberProviderFieldMapping = new Dictionary(); + ContentApps = new List(); } [DataMember(Name = "username")] @@ -34,5 +36,7 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "fieldConfig")] public IDictionary MemberProviderFieldMapping { get; set; } + [DataMember(Name = "apps")] + public IEnumerable ContentApps { get; set; } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs index 36c1b360b2..cf1bc3c253 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs @@ -70,8 +70,13 @@ namespace Umbraco.Web.Models.Mapping dest.Culture = culture; + // Get the segment, which is always allowed to be null even if the propertyType *can* be varied by segment. + // There is therefore no need to perform the null check like with culture above. + var segment = !property.PropertyType.VariesBySegment() ? null : context.GetSegment(); + dest.Segment = segment; + // if no 'IncludeProperties' were specified or this property is set to be included - we will map the value and return. - dest.Value = editor.GetValueEditor().ToEditor(property, DataTypeService, culture); + dest.Value = editor.GetValueEditor().ToEditor(property, DataTypeService, culture, segment); } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs index c279ae2c70..5d076812f3 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs @@ -21,52 +21,117 @@ namespace Umbraco.Web.Models.Mapping public IEnumerable Map(IContent source, MapperContext context) { - var result = new List(); - if (!source.ContentType.VariesByCulture()) + var variesByCulture = source.ContentType.VariesByCulture(); + var variesBySegment = source.ContentType.VariesBySegment(); + + IList variants = new List(); + + if (!variesByCulture && !variesBySegment) { - //this is invariant so just map the IContent instance to ContentVariationDisplay - result.Add(context.Map(source)); + // this is invariant so just map the IContent instance to ContentVariationDisplay + var variantDisplay = context.Map(source); + variants.Add(variantDisplay); + } + else if (variesByCulture && !variesBySegment) + { + var languages = GetLanguages(context); + variants = languages + .Select(language => CreateVariantDisplay(context, source, language, null)) + .ToList(); + } + else if (variesBySegment && !variesByCulture) + { + // Segment only + var segments = GetSegments(source); + variants = segments + .Select(segment => CreateVariantDisplay(context, source, null, segment)) + .ToList(); } else { - var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); - if (allLanguages.Count == 0) return Enumerable.Empty(); //this should never happen + // Culture and segment + var languages = GetLanguages(context).ToList(); + var segments = GetSegments(source).ToList(); - var langs = context.MapEnumerable(allLanguages).ToList(); - - //create a variant for each language, then we'll populate the values - var variants = langs.Select(x => + if (languages.Count == 0 || segments.Count == 0) { - //We need to set the culture in the mapping context since this is needed to ensure that the correct property values - //are resolved during the mapping - context.SetCulture(x.IsoCode); - return context.Map(source); - }).ToList(); - - for (int i = 0; i < langs.Count; i++) - { - var x = langs[i]; - var variant = variants[i]; - - variant.Language = x; - variant.Name = source.GetCultureName(x.IsoCode); + // This should not happen + throw new InvalidOperationException("No languages or segments available"); } - //Put the default language first in the list & then sort rest by a-z - var defaultLang = variants.SingleOrDefault(x => x.Language.IsDefault); + variants = languages + .SelectMany(language => segments + .Select(segment => CreateVariantDisplay(context, source, language, segment))) + .ToList(); + } - //Remove the default language from the list for now - variants.Remove(defaultLang); - - //Sort the remaining languages a-z - variants = variants.OrderBy(x => x.Language.Name).ToList(); - - //Insert the default language as the first item - variants.Insert(0, defaultLang); + return SortVariants(variants); + } + private IList SortVariants(IList variants) + { + if (variants == null || variants.Count <= 1) + { return variants; } - return result; + + // Default variant first, then order by language, segment. + return variants + .OrderBy(v => IsDefaultLanguage(v) ? 0 : 1) + .ThenBy(v => IsDefaultSegment(v) ? 0 : 1) + .ThenBy(v => v?.Language?.Name) + .ThenBy(v => v.Segment) + .ToList(); + } + + private static bool IsDefaultSegment(ContentVariantDisplay variant) + { + return variant.Segment == null; + } + + private static bool IsDefaultLanguage(ContentVariantDisplay variant) + { + return variant.Language == null || variant.Language.IsDefault; + } + + private IEnumerable GetLanguages(MapperContext context) + { + var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); + if (allLanguages.Count == 0) + { + // This should never happen + return Enumerable.Empty(); + } + else + { + return context.MapEnumerable(allLanguages).ToList(); + } + } + + /// + /// Returns all segments assigned to the content + /// + /// + /// + /// Returns all segments assigned to the content including 'null' values + /// + private IEnumerable GetSegments(IContent content) + { + return content.Properties.SelectMany(p => p.Values.Select(v => v.Segment)).Distinct(); + } + + private ContentVariantDisplay CreateVariantDisplay(MapperContext context, IContent content, Language language, string segment) + { + context.SetCulture(language?.IsoCode); + context.SetSegment(segment); + + var variantDisplay = context.Map(content); + + variantDisplay.Segment = segment; + variantDisplay.Language = language; + variantDisplay.Name = content.GetCultureName(language?.IsoCode); + + return variantDisplay; } } } diff --git a/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs index e5bca22287..e654fc16a1 100644 --- a/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Umbraco.Core; using Umbraco.Core.Logging; @@ -23,6 +24,7 @@ namespace Umbraco.Web.Models.Mapping public void DefineMaps(UmbracoMapper mapper) { mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new MacroDisplay(), Map); mapper.Define>((source, context) => context.MapEnumerable(source.Properties.Values)); mapper.Define((source, context) => new MacroParameter(), Map); } @@ -40,6 +42,23 @@ namespace Umbraco.Web.Models.Mapping target.Udi = Udi.Create(Constants.UdiEntityType.Macro, source.Key); } + private void Map(IMacro source, MacroDisplay target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = Constants.Icons.Macro; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.Udi = Udi.Create(Constants.UdiEntityType.Macro, source.Key); + target.CacheByPage = source.CacheByPage; + target.CacheByUser = source.CacheByMember; + target.CachePeriod = source.CacheDuration; + target.UseInEditor = source.UseInEditor; + target.RenderInEditor = !source.DontRender; + target.View = source.MacroSource; + } // Umbraco.Code.MapAll -Value private void Map(IMacroProperty source, MacroParameter target, MapperContext context) { diff --git a/src/Umbraco.Web/Models/Mapping/MapperContextExtensions.cs b/src/Umbraco.Web/Models/Mapping/MapperContextExtensions.cs index 1538f1a987..20a387c679 100644 --- a/src/Umbraco.Web/Models/Mapping/MapperContextExtensions.cs +++ b/src/Umbraco.Web/Models/Mapping/MapperContextExtensions.cs @@ -8,6 +8,7 @@ namespace Umbraco.Web.Models.Mapping internal static class MapperContextExtensions { private const string CultureKey = "Map.Culture"; + private const string SegmentKey = "Map.Segment"; private const string IncludedPropertiesKey = "Map.IncludedProperties"; /// @@ -18,6 +19,14 @@ namespace Umbraco.Web.Models.Mapping return context.HasItems && context.Items.TryGetValue(CultureKey, out var obj) && obj is string s ? s : null; } + /// + /// Gets the context segment. + /// + public static string GetSegment(this MapperContext context) + { + return context.HasItems && context.Items.TryGetValue(SegmentKey, out var obj) && obj is string s ? s : null; + } + /// /// Sets a context culture. /// @@ -26,6 +35,14 @@ namespace Umbraco.Web.Models.Mapping context.Items[CultureKey] = culture; } + /// + /// Sets a context segment. + /// + public static void SetSegment(this MapperContext context, string segment) + { + context.Items[SegmentKey] = segment; + } + /// /// Get included properties. /// @@ -42,4 +59,4 @@ namespace Umbraco.Web.Models.Mapping context.Items[IncludedPropertiesKey] = properties; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs index 8671bfe538..851f452244 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs @@ -76,6 +76,7 @@ namespace Umbraco.Web.Models.Mapping // Umbraco.Code.MapAll -Trashed -IsContainer -VariesByCulture private void Map(IMember source, MemberDisplay target, MapperContext context) { + target.ContentApps = _commonMapper.GetContentApps(source); target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.ContentTypeName = source.ContentType.Name; diff --git a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs index 5ac4037fdb..0adb654d45 100644 --- a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs +++ b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs @@ -215,7 +215,8 @@ namespace Umbraco.Web.Mvc markupToInject = string.Format(Current.Configs.Settings().Content.PreviewBadge, IOHelper.ResolveUrl(SystemDirectories.Umbraco), - Server.UrlEncode(Current.UmbracoContext.HttpContext.Request.Url?.PathAndQuery)); + Server.UrlEncode(Current.UmbracoContext.HttpContext.Request.Url?.PathAndQuery), + Current.UmbracoContext.PublishedRequest.PublishedContent.Id); } else { diff --git a/src/Umbraco.Web/Mvc/ValidateMvcAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web/Mvc/ValidateMvcAngularAntiForgeryTokenAttribute.cs index 756ca7f05c..0803941a70 100644 --- a/src/Umbraco.Web/Mvc/ValidateMvcAngularAntiForgeryTokenAttribute.cs +++ b/src/Umbraco.Web/Mvc/ValidateMvcAngularAntiForgeryTokenAttribute.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.Mvc var userIdentity = filterContext.HttpContext.User.Identity as ClaimsIdentity; if (userIdentity != null) { - //if there is not CookiePath claim, then exist + //if there is not CookiePath claim, then exit if (userIdentity.HasClaim(x => x.Type == ClaimTypes.CookiePath) == false) { base.OnActionExecuting(filterContext); diff --git a/src/Umbraco.Web/Properties/AssemblyInfo.cs b/src/Umbraco.Web/Properties/AssemblyInfo.cs index 9f5abb99b5..ce2cbf0282 100644 --- a/src/Umbraco.Web/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Web/Properties/AssemblyInfo.cs @@ -12,7 +12,7 @@ using System.Runtime.InteropServices; // Umbraco Cms [assembly: InternalsVisibleTo("Umbraco.Web.UI")] - +[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.Embedded")] [assembly: InternalsVisibleTo("Umbraco.Tests")] [assembly: InternalsVisibleTo("Umbraco.Tests.Benchmarks")] diff --git a/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs b/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs index 7c9a549aef..74ea517fa2 100644 --- a/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs @@ -14,7 +14,7 @@ namespace Umbraco.Web.PropertyEditors public JObject Items { get; set; } // TODO: Make these strongly typed, for now this works though - [ConfigurationField("rte", "Rich text editor", "views/propertyeditors/rte/rte.prevalues.html", Description = "Rich text editor configuration")] + [ConfigurationField("rte", "Rich text editor", "views/propertyeditors/rte/rte.prevalues.html", Description = "Rich text editor configuration", HideLabel = true)] public JObject Rte { get; set; } [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index da4264be28..7134fe8703 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -128,10 +128,10 @@ namespace Umbraco.Web.PropertyEditors /// public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { - var val = property.GetValue(culture, segment); - if (val == null) return string.Empty; + var val = property.GetValue(culture, segment)?.ToString(); + if (val.IsNullOrWhiteSpace()) return string.Empty; - var grid = DeserializeGridValue(val.ToString(), out var rtes, out _); + var grid = DeserializeGridValue(val, out var rtes, out _); //process the rte values foreach (var rte in rtes.ToList()) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index f07ea7db6b..19aab7ea65 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -207,7 +207,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource VersionDate = dto.EditVersionDate, WriterId = dto.EditWriterId, Properties = nested.PropertyData, - CultureInfos = nested.CultureData + CultureInfos = nested.CultureData, + UrlSegment = nested.UrlSegment }; } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index a866297d72..e0b95e8481 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -54,6 +54,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly ContentStore _mediaStore; private readonly SnapDictionary _domainStore; private readonly object _storesLock = new object(); + private readonly object _elementsLock = new object(); private BPlusTree _localContentDb; private BPlusTree _localMediaDb; @@ -1131,7 +1132,13 @@ namespace Umbraco.Web.PublishedCache.NuCache ContentStore.Snapshot contentSnap, mediaSnap; SnapDictionary.Snapshot domainSnap; IAppCache elementsCache; - lock (_storesLock) + + // Here we are reading/writing to shared objects so we need to lock (can't be _storesLock which manages the actual nucache files + // and would result in a deadlock). Even though we are locking around underlying readlocks (within CreateSnapshot) it's because + // we need to ensure that the result of contentSnap.Gen (etc) and the re-assignment of these values and _elements cache + // are done atomically. + + lock (_elementsLock) { var scopeContext = _scopeProvider.Context; diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 2dbe4de4c5..d697898f33 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -183,54 +183,62 @@ namespace Umbraco.Web #region Search /// - public IEnumerable Search(string term, string culture = "*", string indexName = null) + public IEnumerable Search(string term, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName) { return Search(term, 0, 0, out _, culture, indexName); } /// - public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null) + public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName) { - indexName = string.IsNullOrEmpty(indexName) - ? Constants.UmbracoIndexes.ExternalIndexName - : indexName; + if (skip < 0) + { + throw new ArgumentOutOfRangeException(nameof(skip), skip, "The value must be greater than or equal to zero."); + } + + if (take < 0) + { + throw new ArgumentOutOfRangeException(nameof(take), take, "The value must be greater than or equal to zero."); + } + + if (string.IsNullOrEmpty(indexName)) + { + indexName = Constants.UmbracoIndexes.ExternalIndexName; + } if (!_examineManager.TryGetIndex(indexName, out var index) || !(index is IUmbracoIndex umbIndex)) + { throw new InvalidOperationException($"No index found by name {indexName} or is not of type {typeof(IUmbracoIndex)}"); + } - var searcher = umbIndex.GetSearcher(); + var query = umbIndex.GetSearcher().CreateQuery(IndexTypes.Content); - // default to max 500 results - var count = skip == 0 && take == 0 ? 500 : skip + take; - - ISearchResults results; + IQueryExecutor queryExecutor; if (culture == "*") { - //search everything - - results = searcher.Search(term, count); + // Search everything + queryExecutor = query.ManagedQuery(term); } - else if (culture.IsNullOrWhiteSpace()) + else if (string.IsNullOrWhiteSpace(culture)) { - //only search invariant - - var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, "n"); //must not vary by culture - qry = qry.And().ManagedQuery(term); - results = qry.Execute(count); + // Only search invariant + queryExecutor = query.Field(UmbracoContentIndex.VariesByCultureFieldName, "n") // Must not vary by culture + .And().ManagedQuery(term); } else { - //search only the specified culture - - //get all index fields suffixed with the culture name supplied - var cultureFields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); - var qry = searcher.CreateQuery().ManagedQuery(term, cultureFields); - results = qry.Execute(count); + // Only search the specified culture + var fields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); // Get all index fields suffixed with the culture name supplied + queryExecutor = query.ManagedQuery(term, fields); } + var results = skip == 0 && take == 0 + ? queryExecutor.Execute() + : queryExecutor.Execute(skip + take); + totalRecords = results.TotalItemCount; - return new CultureContextualSearchResults(results.ToPublishedSearchResults(_publishedSnapshot.Content), _variationContextAccessor, culture); + return new CultureContextualSearchResults(results.Skip(skip).ToPublishedSearchResults(_publishedSnapshot.Content), _variationContextAccessor, culture); } /// @@ -242,12 +250,23 @@ namespace Umbraco.Web /// public IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords) { + if (skip < 0) + { + throw new ArgumentOutOfRangeException(nameof(skip), skip, "The value must be greater than or equal to zero."); + } + + if (take < 0) + { + throw new ArgumentOutOfRangeException(nameof(take), take, "The value must be greater than or equal to zero."); + } + var results = skip == 0 && take == 0 ? query.Execute() - : query.Execute(maxResults: skip + take); + : query.Execute(skip + take); totalRecords = results.TotalItemCount; - return results.ToPublishedSearchResults(_publishedSnapshot.Content); + + return results.Skip(skip).ToPublishedSearchResults(_publishedSnapshot); } /// @@ -320,9 +339,6 @@ namespace Umbraco.Web } } - - - #endregion } } diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index c07430df04..9a22c59566 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -3,6 +3,8 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Sync; @@ -11,14 +13,16 @@ namespace Umbraco.Web.Scheduling internal class KeepAlive : RecurringTaskBase { private readonly IRuntimeState _runtime; + private readonly IKeepAliveSection _keepAliveSection; private readonly IProfilingLogger _logger; private static HttpClient _httpClient; public KeepAlive(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, - IRuntimeState runtime, IProfilingLogger logger) + IRuntimeState runtime, IKeepAliveSection keepAliveSection, IProfilingLogger logger) : base(runner, delayMilliseconds, periodMilliseconds) { _runtime = runtime; + _keepAliveSection = keepAliveSection; _logger = logger; if (_httpClient == null) _httpClient = new HttpClient(); @@ -46,25 +50,27 @@ namespace Umbraco.Web.Scheduling using (_logger.DebugDuration("Keep alive executing", "Keep alive complete")) { - string umbracoAppUrl = null; - + var keepAlivePingUrl = _keepAliveSection.KeepAlivePingUrl; try { - umbracoAppUrl = _runtime.ApplicationUrl.ToString(); - if (umbracoAppUrl.IsNullOrWhiteSpace()) + if (keepAlivePingUrl.Contains("{umbracoApplicationUrl}")) { - _logger.Warn("No url for service (yet), skip."); - return true; // repeat + var umbracoAppUrl = _runtime.ApplicationUrl.ToString(); + if (umbracoAppUrl.IsNullOrWhiteSpace()) + { + _logger.Warn("No umbracoApplicationUrl for service (yet), skip."); + return true; // repeat + } + + keepAlivePingUrl = keepAlivePingUrl.Replace("{umbracoApplicationUrl}", umbracoAppUrl.TrimEnd('/')); } - var url = umbracoAppUrl.TrimEnd('/') + "/api/keepalive/ping"; - - var request = new HttpRequestMessage(HttpMethod.Get, url); + var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl); var result = await _httpClient.SendAsync(request, token); } catch (Exception ex) { - _logger.Error(ex, "Keep alive failed (at '{UmbracoAppUrl}').", umbracoAppUrl); + _logger.Error(ex, "Keep alive failed (at '{keepAlivePingUrl}').", keepAlivePingUrl); } } diff --git a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs index 1416393f46..a08289186f 100644 --- a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs +++ b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs @@ -99,7 +99,11 @@ namespace Umbraco.Web.Scheduling var tasks = new List(); - tasks.Add(RegisterKeepAlive()); + if (settings.KeepAlive.DisableKeepAliveTask == false) + { + tasks.Add(RegisterKeepAlive(settings.KeepAlive)); + } + tasks.Add(RegisterScheduledPublishing()); tasks.Add(RegisterLogScrubber(settings)); tasks.Add(RegisterTempFileCleanup()); @@ -112,11 +116,11 @@ namespace Umbraco.Web.Scheduling }); } - private IBackgroundTask RegisterKeepAlive() + private IBackgroundTask RegisterKeepAlive(IKeepAliveSection keepAliveSection) { // ping/keepalive // on all servers - var task = new KeepAlive(_keepAliveRunner, DefaultDelayMilliseconds, FiveMinuteMilliseconds, _runtime, _logger); + var task = new KeepAlive(_keepAliveRunner, DefaultDelayMilliseconds, FiveMinuteMilliseconds, _runtime, keepAliveSection, _logger); _keepAliveRunner.TryAdd(task); return task; } diff --git a/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs b/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs index e7aef4be03..ed6abb3fbe 100644 --- a/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs +++ b/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs @@ -41,6 +41,9 @@ namespace Umbraco.Web.Security : Guid.NewGuid(); backOfficeIdentity.SessionId = session.ToString(); + + //since it is a cookie-based authentication add that claim + backOfficeIdentity.AddClaim(new Claim(ClaimTypes.CookiePath, "/", ClaimValueTypes.String, UmbracoBackOfficeIdentity.Issuer, UmbracoBackOfficeIdentity.Issuer, backOfficeIdentity)); } base.ResponseSignIn(context); diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 3373a6fa8b..85ff30487d 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -129,8 +129,8 @@ namespace Umbraco.Web.Trees menu.DefaultMenuAlias = ActionNew.ActionAlias; // we need to get the default permissions as you can't set permissions on the very root node - var permission = Services.UserService.GetPermissions(Security.CurrentUser, Constants.System.Root).First(); - var nodeActions = _actions.FromEntityPermission(permission) + var assignedPermissions = Services.UserService.GetAssignedPermissions(Security.CurrentUser, Constants.System.Root); + var nodeActions = _actions.GetByLetters(assignedPermissions) .Select(x => new MenuItem(x)); //these two are the standard items diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index c3458f93a2..1069df0ec4 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -195,20 +195,30 @@ namespace Umbraco.Web.Trees //get the current user start node/paths GetUserStartNodes(out var userStartNodes, out var userStartNodePaths); - nodes.AddRange(entities.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings, userStartNodes, userStartNodePaths)).Where(x => x != null)); - // if the user does not have access to the root node, what we have is the start nodes, - // but to provide some context we also need to add their topmost nodes when they are not + // but to provide some context we need to add their topmost nodes when they are not // topmost nodes themselves (level > 1). if (id == rootIdString && hasAccessToRoot == false) { - var topNodeIds = entities.Where(x => x.Level > 1).Select(GetTopNodeId).Where(x => x != 0).Distinct().ToArray(); + // first add the entities that are topmost to the nodes collection + var topMostEntities = entities.Where(x => x.Level == 1).ToArray(); + nodes.AddRange(topMostEntities.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings, userStartNodes, userStartNodePaths)).Where(x => x != null)); + + // now add the topmost nodes of the entities that aren't topmost to the nodes collection as well + // - these will appear as "no-access" nodes in the tree, but will allow the editors to drill down through the tree + // until they reach their start nodes + var topNodeIds = entities.Except(topMostEntities).Select(GetTopNodeId).Where(x => x != 0).Distinct().ToArray(); if (topNodeIds.Length > 0) { var topNodes = Services.EntityService.GetAll(UmbracoObjectType, topNodeIds.ToArray()); nodes.AddRange(topNodes.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings, userStartNodes, userStartNodePaths)).Where(x => x != null)); } } + else + { + // the user has access to the root, just add the entities + nodes.AddRange(entities.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings, userStartNodes, userStartNodePaths)).Where(x => x != null)); + } return nodes; } @@ -454,8 +464,8 @@ namespace Umbraco.Web.Trees internal IEnumerable GetAllowedUserMenuItemsForNode(IUmbracoEntity dd) { - var permission = Services.UserService.GetPermissions(Security.CurrentUser, dd.Path); - return Current.Actions.FromEntityPermission(permission).Select(x => new MenuItem(x)); + var permissionsForPath = Services.UserService.GetPermissionsForPath(Security.CurrentUser, dd.Path).GetAllPermissions(); + return Current.Actions.GetByLetters(permissionsForPath).Select(x => new MenuItem(x)); } /// diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index 4e5b1df631..6a7fb7f5ad 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -60,19 +60,20 @@ namespace Umbraco.Web.Trees //System ListView nodes var systemListViewDataTypeIds = GetNonDeletableSystemListViewDataTypeIds(); + var children = Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.DataType).ToArray(); + var dataTypes = Services.DataTypeService.GetAll(children.Select(c => c.Id).ToArray()).ToDictionary(dt => dt.Id); + nodes.AddRange( - Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.DataType) + children .OrderBy(entity => entity.Name) .Select(dt => { - var node = CreateTreeNode(dt.Id.ToInvariantString(), id, queryStrings, dt.Name, Constants.Icons.DataType, false); + var dataType = dataTypes[dt.Id]; + var node = CreateTreeNode(dt.Id.ToInvariantString(), id, queryStrings, dt.Name, dataType.Editor.Icon, false); node.Path = dt.Path; - if (systemListViewDataTypeIds.Contains(dt.Id)) - { - node.Icon = Constants.Icons.ListView; - } return node; - })); + }) + ); return nodes; } diff --git a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs index bd80f63897..2046baf2d3 100644 --- a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs @@ -2,7 +2,10 @@ using System.Linq; using System.Net.Http.Formatting; using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Trees; +using Umbraco.Web.Search; using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Trees @@ -10,8 +13,15 @@ namespace Umbraco.Web.Trees [CoreTree] [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] [Tree(Constants.Applications.Settings, Constants.Trees.MemberTypes, SortOrder = 2, TreeGroup = Constants.Trees.Groups.Settings)] - public class MemberTypeTreeController : MemberTypeAndGroupTreeControllerBase + public class MemberTypeTreeController : MemberTypeAndGroupTreeControllerBase, ISearchableTree { + private readonly UmbracoTreeSearcher _treeSearcher; + + public MemberTypeTreeController(UmbracoTreeSearcher treeSearcher) + { + _treeSearcher = treeSearcher; + } + protected override TreeNode CreateRootNode(FormDataCollection queryStrings) { var root = base.CreateRootNode(queryStrings); @@ -25,5 +35,9 @@ namespace Umbraco.Web.Trees .OrderBy(x => x.Name) .Select(dt => CreateTreeNode(dt, Constants.ObjectTypes.MemberType, id, queryStrings, Constants.Icons.MemberType, false)); } + + public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) + => _treeSearcher.EntitySearch(UmbracoObjectTypes.MemberType, query, pageSize, pageIndex, out totalFound, searchFrom); + } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 4a1cf2c4c8..73f9da29ba 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -63,7 +63,7 @@ - + 2.7.0.100 diff --git a/src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs index 0abdfb5d2f..f147a2a4cb 100644 --- a/src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs @@ -21,7 +21,7 @@ namespace Umbraco.Web.WebApi.Filters var userIdentity = ((ApiController) actionContext.ControllerContext.Controller).User.Identity as ClaimsIdentity; if (userIdentity != null) { - //if there is not CookiePath claim, then exist + //if there is not CookiePath claim, then exit if (userIdentity.HasClaim(x => x.Type == ClaimTypes.CookiePath) == false) { base.OnActionExecuting(actionContext); diff --git a/src/umbraco.sln b/src/umbraco.sln index 0c5f02c7bd..a747f21d19 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -102,6 +102,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IssueTemplates", "IssueTemp ..\.github\ISSUE_TEMPLATE\5_Security_issue.md = ..\.github\ISSUE_TEMPLATE\5_Security_issue.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.ModelsBuilder.Embedded", "Umbraco.ModelsBuilder.Embedded\Umbraco.ModelsBuilder.Embedded.csproj", "{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.TestData", "Umbraco.TestData\Umbraco.TestData.csproj", "{FB5676ED-7A69-492C-B802-E7B24144C0FC}" EndProject Global @@ -136,6 +138,10 @@ Global {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.Build.0 = Release|Any CPU + {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.Build.0 = Release|Any CPU {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU