Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 350f60c056 | |||
| 954b5ac296 | |||
| 12787f0bd2 | |||
| 6cf15f42a8 | |||
| b5b971a189 | |||
| 26a026b17a | |||
| 9054b84c50 | |||
| 5cb5f13d08 | |||
| 89468fce97 | |||
| a1997291ab | |||
| 1f8fae0dd4 | |||
| 3bd2f766ef | |||
| f8ed341eec | |||
| f3b745f5b3 | |||
| 8d23a9b480 | |||
| c2e89ed955 | |||
| 782fc26723 | |||
| 378c2f727c | |||
| 5c983bce5f | |||
| 4eb18855e9 | |||
| 2200839bfa | |||
| 796cb9c35b | |||
| ac0d21cb3d | |||
| 70951317c5 | |||
| 4270a6c440 | |||
| 030688f1a2 | |||
| b11b2c0a71 | |||
| 7680ce4ef1 | |||
| ab872b7ca4 | |||
| 8533210761 | |||
| 673575594d | |||
| 4702bdab33 | |||
| ab08f1fc06 | |||
| 6b29bbc9e7 | |||
| 80f92311dc | |||
| c1723cbda6 | |||
| 5ee0311371 | |||
| d6c7ba03e6 | |||
| 32ecf932a6 | |||
| 26d16e3897 | |||
| ab0074e07c | |||
| 71c8e20a79 | |||
| 58bd7e8453 | |||
| d57c168f82 | |||
| 4a5a7068bc | |||
| d34649ab1a | |||
| 24407ead18 | |||
| ad4f0cf9d1 | |||
| f5efb7609a | |||
| ab82d3b053 | |||
| 36e0673c83 | |||
| 9d27e7386e | |||
| 8502e10900 | |||
| 0a701d0c64 | |||
| ceaa15aa82 | |||
| 2445e0cbcb | |||
| 5a5ad350d6 | |||
| 40e986d5dc | |||
| c81b68e9a8 | |||
| 8e9f1bc949 | |||
| e8b7ffd6f5 | |||
| b12eecfb1d | |||
| 06ca34f434 | |||
| 5880b1b612 | |||
| 12fc88aca9 | |||
| e56db8baa4 | |||
| c7aae533ad | |||
| acaa062b7d | |||
| f9928906fa | |||
| d3719122ce | |||
| 1e3fbf4e2f | |||
| fc8130987c | |||
| f3dc8f592b | |||
| 8fda46f0ad | |||
| 8d3c9caafb | |||
| 23031a64d1 | |||
| 37b27f5e73 |
+2
-1
@@ -13,6 +13,7 @@
|
||||
*.suo
|
||||
*.vs10x
|
||||
*.ndproj
|
||||
*.log
|
||||
|
||||
# common directories
|
||||
.DS_Store
|
||||
@@ -169,4 +170,4 @@ build/temp/
|
||||
|
||||
|
||||
|
||||
# eof
|
||||
# eof
|
||||
|
||||
@@ -48,10 +48,15 @@
|
||||
<dependency id="Newtonsoft.Json" version="[11.0.2,11.666666)" />
|
||||
<dependency id="NPoco" version="[3.9.3,3.666666)" />
|
||||
<dependency id="Semver" version="[2.0.4,2.666666)" />
|
||||
<dependency id="System.Reflection.Metadata" version="[1.5.0,1.666666)" />
|
||||
<dependency id="System.Reflection.Primitives" version="[4.3.0,4.666666)" />
|
||||
<dependency id="System.Runtime.Handles" version="[4.3.0,4.666666)" />
|
||||
<dependency id="System.Security.Cryptography.Algorithms" version="[4.3.1,4.666666)" />
|
||||
<dependency id="System.Security.Cryptography.X509Certificates" version="[4.3.2,4.666666)" />
|
||||
<dependency id="System.Text.Encoding.CodePages" version="[4.4.0,4.666666)" />
|
||||
<dependency id="System.Threading.Tasks.Dataflow" version="[4.8.0,4.666666)" />
|
||||
<dependency id="System.ValueTuple" version="[4.4.0,4.666666)" />
|
||||
<dependency id="Umbraco.ModelsBuilder" version="[8.0.0-alpha.18,8.666666)" />
|
||||
<dependency id="Umbraco.SqlServerCE" version="[4.0.0.1,4.666666)" />
|
||||
</dependencies>
|
||||
</metadata>
|
||||
@@ -60,14 +65,12 @@
|
||||
<file src="$BuildTmp$\WebApp\bin\Umbraco.Web.dll" target="lib\net472\Umbraco.Web.dll" />
|
||||
<file src="$BuildTmp$\WebApp\bin\Umbraco.Web.UI.dll" target="lib\net472\Umbraco.Web.UI.dll" />
|
||||
<file src="$BuildTmp$\WebApp\bin\Umbraco.Examine.dll" target="lib\net472\Umbraco.Examine.dll" />
|
||||
<file src="$BuildTmp$\WebApp\bin\Umbraco.ModelsBuilder.dll" target="lib\net472\Umbraco.ModelsBuilder.dll" />
|
||||
|
||||
<!-- docs -->
|
||||
<!-- docs -->
|
||||
<file src="$BuildTmp$\WebApp\bin\Umbraco.Core.xml" target="lib\Umbraco.Core.xml" />
|
||||
<file src="$BuildTmp$\WebApp\bin\Umbraco.Web.xml" target="lib\Umbraco.Web.xml" />
|
||||
<file src="$BuildTmp$\WebApp\bin\Umbraco.Web.UI.xml" target="lib\Umbraco.Web.UI.xml" />
|
||||
<file src="$BuildTmp$\WebApp\bin\Umbraco.Examine.xml" target="lib\Umbraco.Examine.xml" />
|
||||
<file src="$BuildTmp$\WebApp\bin\Umbraco.ModelsBuilder.xml" target="lib\Umbraco.ModelsBuilder.xml" />
|
||||
|
||||
<file src="tools\install.core.ps1" target="tools\install.ps1" />
|
||||
|
||||
@@ -80,7 +83,5 @@
|
||||
<file src="$BuildTmp$\..\src\Umbraco.Web.UI\**\*.cs" exclude="$BuildTmp$\..\src\**\TemporaryGeneratedFile*.cs" target="src\Umbraco.Web.UI" />
|
||||
<file src="$BuildTmp$\bin\Umbraco.Examine.pdb" target="lib" />
|
||||
<file src="$BuildTmp$\..\src\Umbraco.Examine\**\*.cs" exclude="$BuildTmp$\..\src\**\TemporaryGeneratedFile*.cs" target="src\Umbraco.Examine" />
|
||||
<file src="$BuildTmp$\bin\Umbraco.ModelsBuilder.pdb" target="lib" />
|
||||
<file src="$BuildTmp$\..\src\Umbraco.ModelsBuilder\**\*.cs" exclude="$BuildTmp$\..\src\**\TemporaryGeneratedFile*.cs" target="src\Umbraco.ModelsBuilder" />
|
||||
</files>
|
||||
</package>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<tags>umbraco</tags>
|
||||
<dependencies>
|
||||
<dependency id="UmbracoCms.Core" version="[$version$]" />
|
||||
<dependency id="Umbraco.ModelsBuilder" version="[8.0.0-alpha.18, 9.0.0)" />
|
||||
<dependency id="Microsoft.AspNet.SignalR.Core" version="[2.2.3, 3.0.0)" />
|
||||
</dependencies>
|
||||
</metadata>
|
||||
@@ -32,11 +33,6 @@
|
||||
<file src="$BuildTmp$\WebApp\umbraco_client\**" target="UmbracoFiles\umbraco_client" />
|
||||
<file src="$BuildTmp$\WebApp\Media\Web.config" target="Content\Media\Web.config" />
|
||||
|
||||
<file src="$BuildTmp$\WebApp\App_Plugins\ModelsBuilder\modelsbuilder.htm" target="Content\App_Plugins\ModelsBuilder\modelsbuilder.htm" />
|
||||
<file src="$BuildTmp$\WebApp\App_Plugins\ModelsBuilder\modelsbuilder.controller.js" target="Content\App_Plugins\ModelsBuilder\modelsbuilder.controller.js" />
|
||||
<file src="$BuildTmp$\WebApp\App_Plugins\ModelsBuilder\modelsbuilder.resource.js" target="Content\App_Plugins\ModelsBuilder\modelsbuilder.resource.js" />
|
||||
<file src="$BuildTmp$\WebApp\App_Plugins\ModelsBuilder\package.manifest" target="Content\App_Plugins\ModelsBuilder\package.manifest" />
|
||||
|
||||
<file src="tools\install.ps1" target="tools\install.ps1" />
|
||||
<file src="tools\Readme.txt" target="tools\Readme.txt" />
|
||||
<file src="tools\ReadmeUpgrade.txt" target="tools\ReadmeUpgrade.txt" />
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
|
||||
<configSections xdt:Transform="InsertIfMissing" />
|
||||
<configSections>
|
||||
<section name="BaseRestExtensions" xdt:Locator="Match(name)" xdt:Transform="Remove" />
|
||||
<section name="FileSystemProviders" xdt:Locator="Match(name)" xdt:Transform="Remove" />
|
||||
<section name="ExamineLuceneIndexSets" type="Umbraco.Examine.Config.IndexSets, Umbraco.Examine" requirePermission="false" xdt:Locator="Match(name)" xdt:Transform="SetAttributes(type,requirePermission)" />
|
||||
|
||||
|
||||
<sectionGroup name="applicationSettings" xdt:Locator="Match(name)">
|
||||
<section name="umbraco.presentation.Properties.Settings" xdt:Locator="Match(name)" xdt:Transform="Remove" />
|
||||
</sectionGroup>
|
||||
<sectionGroup name="system.web.webPages.razor" xdt:Locator="Match(name)" xdt:Transform="Remove" />
|
||||
<sectionGroup name="umbracoConfiguration" xdt:Locator="Match(name)" xdt:Transform="InsertIfMissing">
|
||||
<section name="settings" type="Umbraco.Core.Configuration.UmbracoSettings.UmbracoSettingsSection, Umbraco.Core" requirePermission="false" xdt:Locator="Match(name)" xdt:Transform="InsertIfMissing" />
|
||||
<section name="BaseRestExtensions" type="Umbraco.Core.Configuration.BaseRest.BaseRestSection, Umbraco.Core" requirePermission="false" xdt:Locator="Match(name)" xdt:Transform="InsertIfMissing" />
|
||||
<section name="FileSystemProviders" type="Umbraco.Core.Configuration.FileSystemProvidersSection, Umbraco.Core" requirePermission="false" xdt:Locator="Match(name)" xdt:Transform="InsertIfMissing" />
|
||||
<section name="dashBoard" type="Umbraco.Core.Configuration.Dashboard.DashboardSection, Umbraco.Core" requirePermission="false" xdt:Locator="Match(name)" xdt:Transform="InsertIfMissing" />
|
||||
<section name="HealthChecks" type="Umbraco.Core.Configuration.HealthChecks.HealthChecksSection, Umbraco.Core" requirePermission="false" xdt:Locator="Match(name)" xdt:Transform="InsertIfMissing" />
|
||||
@@ -27,12 +29,14 @@
|
||||
|
||||
<umbracoConfiguration xdt:Transform="InsertIfMissing">
|
||||
<settings configSource="config\umbracoSettings.config" xdt:Locator="Match(configSource)" xdt:Transform="InsertIfMissing" />
|
||||
<BaseRestExtensions configSource="config\BaseRestExtensions.config" xdt:Locator="Match(configSource)" xdt:Transform="InsertIfMissing" />
|
||||
<FileSystemProviders configSource="config\FileSystemProviders.config" xdt:Locator="Match(configSource)" xdt:Transform="InsertIfMissing" />
|
||||
<dashBoard configSource="config\Dashboard.config" xdt:Locator="Match(configSource)" xdt:Transform="InsertIfMissing" />
|
||||
<HealthChecks configSource="config\HealthChecks.config" xdt:Locator="Match(configSource)" xdt:Transform="InsertIfMissing" />
|
||||
</umbracoConfiguration>
|
||||
|
||||
<FileSystemProviders xdt:Transform="Remove" />
|
||||
<BaseRestExtensions xdt:Transform="Remove" />
|
||||
|
||||
<system.data xdt:Transform="InsertIfMissing">
|
||||
<DbProviderFactories xdt:Transform="InsertIfMissing">
|
||||
@@ -57,7 +61,7 @@
|
||||
<httpRuntime xdt:Transform="InsertIfMissing" />
|
||||
<httpRuntime maxRequestLength="51200" fcnMode="Single" xdt:Transform="SetAttributes(fcnMode,maxRequestLength)" />
|
||||
<httpRuntime targetFramework="4.5" xdt:Locator="Condition(count(@targetFramework) != 1)" xdt:Transform="SetAttributes(targetFramework)" />
|
||||
|
||||
|
||||
<membership defaultProvider="DefaultMembershipProvider" xdt:Locator="Match(defaultProvider)" xdt:Transform="Remove" />
|
||||
<roleManager defaultProvider="DefaultRoleProvider" xdt:Locator="Match(defaultProvider)" xdt:Transform="Remove"/>
|
||||
<profile defaultProvider="DefaultProfileProvider" xdt:Locator="Match(defaultProvider)" xdt:Transform="Remove"/>>
|
||||
@@ -88,6 +92,9 @@
|
||||
<remove name="umbracoRequestModule" xdt:Locator="Match(name)" xdt:Transform="Remove" />
|
||||
<remove name="viewstateMoverModule" xdt:Locator="Match(name)" xdt:Transform="Remove" />
|
||||
<remove name="umbracoBaseRequestModule" xdt:Locator="Match(name)" xdt:Transform="Remove" />
|
||||
<remove name="WebDAVModule" xdt:Locator="Match(name)" xdt:Transform="Remove" />
|
||||
<!-- Inserts it as the first element. Also see http://stackoverflow.com/a/19041487/5018 -->
|
||||
<remove name="WebDAVModule" xdt:Locator="Match(name)" xdt:Transform="Insert" />
|
||||
|
||||
<add name="umbracoRequestModule" xdt:Locator="Match(name)" xdt:Transform="Remove" />
|
||||
<add name="viewstateMoverModule" xdt:Locator="Match(name)" xdt:Transform="Remove" />
|
||||
@@ -150,42 +157,62 @@
|
||||
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="log4net" publicKeyToken="669e0ddf0bb1aa2a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-2.0.8.0" newVersion="2.0.8.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="Microsoft.Owin.Security.Cookies" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="Microsoft.Owin.Security.OAuth" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-11.0.0.0" newVersion="11.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-1.2.1.0" newVersion="1.2.1.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="System.Web.Http" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.2.6.0" newVersion="5.2.6.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.2.6.0" newVersion="5.2.6.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="HtmlAgilityPack" publicKeyToken="bd319b19eaf3b43a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-1.5.1.0" newVersion="1.5.1.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="AutoMapper" publicKeyToken="be96cd2c38ef1005" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.3.1.0" newVersion="3.3.1.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0"/>
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="System.Web.WebPages.Razor" publicKeyToken="31bf3856ad364e35" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="System.Web.Http" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="Microsoft.Owin.Security.OAuth" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="Microsoft.Owin.Security.Cookies" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="System.Net.Http.Formatting" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="log4net" publicKeyToken="669e0ddf0bb1aa2a" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-2.0.8.0" newVersion="2.0.8.0"/>
|
||||
</dependentAssembly>
|
||||
<dependentAssembly xdt:Transform="Insert">
|
||||
<assemblyIdentity name="System.Data.SqlServerCe" publicKeyToken="89845DCD8080CC91" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.0.1" newVersion="4.0.0.1"/>
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
|
||||
@@ -205,4 +232,4 @@
|
||||
</system.webServer>
|
||||
</location>
|
||||
|
||||
</configuration>
|
||||
</configuration>
|
||||
@@ -425,8 +425,6 @@
|
||||
|
||||
$ubuild.DefineMethod("Build",
|
||||
{
|
||||
$error.Clear()
|
||||
|
||||
$this.PrepareBuild()
|
||||
if ($this.OnError()) { return }
|
||||
$this.RestoreNuGet()
|
||||
|
||||
+1
-1
@@ -19,4 +19,4 @@ using System.Resources;
|
||||
|
||||
// these are FYI and changed automatically
|
||||
[assembly: AssemblyFileVersion("8.0.0")]
|
||||
[assembly: AssemblyInformationalVersion("8.0.0-alpha.41")]
|
||||
[assembly: AssemblyInformationalVersion("8.0.0-alpha.37")]
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Umbraco.Core.Configuration
|
||||
/// <summary>
|
||||
/// Gets the version comment of the executing code (eg "beta").
|
||||
/// </summary>
|
||||
public static string CurrentComment => "alpha.41";
|
||||
public static string CurrentComment => "alpha.37";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assembly version of Umbraco.Code.dll.
|
||||
|
||||
@@ -72,7 +72,12 @@ namespace Umbraco.Core
|
||||
/// <summary>
|
||||
/// Grid.
|
||||
/// </summary>
|
||||
public const string Grid = "Umbraco.Grid";
|
||||
public const string Grid = "Umbraco.Grid";
|
||||
|
||||
/// <summary>
|
||||
/// Grid.
|
||||
/// </summary>
|
||||
public const string Grid2 = "Umbraco.Grid2";
|
||||
|
||||
/// <summary>
|
||||
/// Image Cropper.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Core.Models
|
||||
{
|
||||
public class Grid2Cell
|
||||
{
|
||||
public int Colspan { get; set; }
|
||||
|
||||
public IPublishedElement Settings { get; set; }
|
||||
|
||||
public IEnumerable<IPublishedElement> Items { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Core.Models
|
||||
{
|
||||
public class Grid2Row
|
||||
{
|
||||
public string Alias { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public IPublishedElement Settings { get; set; }
|
||||
|
||||
public IEnumerable<Grid2Cell> Cells { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core.Models
|
||||
{
|
||||
public class Grid2Value
|
||||
{
|
||||
public int Columns { get; set; }
|
||||
|
||||
public IEnumerable<Grid2Row> Rows { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,44 @@
|
||||
using System;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a default overridable implementation for <see cref="IPropertyValueConverter"/> that does nothing.
|
||||
/// </summary>
|
||||
public abstract class PropertyValueConverterBase : IPropertyValueConverter
|
||||
{
|
||||
public virtual bool IsConverter(PublishedPropertyType propertyType)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual Type GetPropertyValueType(PublishedPropertyType propertyType)
|
||||
{
|
||||
return typeof (object);
|
||||
}
|
||||
|
||||
public virtual PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType)
|
||||
{
|
||||
return PropertyCacheLevel.Snapshot;
|
||||
}
|
||||
|
||||
public virtual object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
public virtual object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
{
|
||||
return inter;
|
||||
}
|
||||
|
||||
public virtual object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
{
|
||||
return inter?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a default overridable implementation for <see cref="IPropertyValueConverter"/> that does nothing.
|
||||
/// </summary>
|
||||
public abstract class PropertyValueConverterBase : IPropertyValueConverter
|
||||
{
|
||||
public virtual bool IsConverter(PublishedPropertyType propertyType)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual Type GetPropertyValueType(PublishedPropertyType propertyType)
|
||||
{
|
||||
return typeof (object);
|
||||
}
|
||||
|
||||
public virtual PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType)
|
||||
{
|
||||
return PropertyCacheLevel.Snapshot;
|
||||
}
|
||||
|
||||
// TODO: Rename to ConvertSourceToCachableIntermediate
|
||||
public virtual object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// TODO: Rename to ConvertSourceToCachableIntermediate
|
||||
public virtual object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
{
|
||||
return inter;
|
||||
}
|
||||
|
||||
// TODO: Rename to ConvertSourceToCachableIntermediate
|
||||
public virtual object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
{
|
||||
return inter?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1518
-1514
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, string> GetModels(Dictionary<string, string> 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<IDictionary<string, string>>(_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<string, string>("grant_type", "password"),
|
||||
new KeyValuePair<string, string>("userName", _user),
|
||||
new KeyValuePair<string, string>("password", _password),
|
||||
});
|
||||
|
||||
var result = client.PostAsync(_url + UmbracoOAuthTokenUrl, formData).Result;
|
||||
|
||||
EnsureSuccess(result);
|
||||
|
||||
var token = result.Content.ReadAsAsync<TokenData>(_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", "%");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Umbraco.ModelsBuilder.Building;
|
||||
using Umbraco.ModelsBuilder.Umbraco;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Api
|
||||
{
|
||||
// internal to be used by Umbraco.ModelsBuilder.Api project
|
||||
internal static class ApiHelper
|
||||
{
|
||||
public static Dictionary<string, string> GetModels(string modelsNamespace, IDictionary<string, string> files)
|
||||
{
|
||||
var umbraco = ModelsBuilderComponent.Umbraco;
|
||||
var typeModels = umbraco.GetAllTypes();
|
||||
|
||||
var parseResult = new CodeParser().ParseWithReferencedAssemblies(files);
|
||||
var builder = new TextBuilder(typeModels, parseResult, modelsNamespace);
|
||||
|
||||
var models = new Dictionary<string, string>();
|
||||
foreach (var typeModel in builder.GetModelsToGenerate())
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
builder.Generate(sb, typeModel);
|
||||
models[typeModel.ClrName] = sb.ToString();
|
||||
}
|
||||
return models;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages API version handshake between client and server.
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApiVersion"/> class.
|
||||
/// </summary>
|
||||
/// <param name="executingVersion">The currently executing version.</param>
|
||||
/// <param name="minClientVersionSupportedByServer">The min client version supported by the server.</param>
|
||||
/// <param name="minServerVersionSupportingClient">An opt min server version supporting the client.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently executing API version.
|
||||
/// </summary>
|
||||
public static ApiVersion Current { get; }
|
||||
= new ApiVersion(Assembly.GetExecutingAssembly().GetName().Version,
|
||||
MinClientVersionSupportedByServerConst, MinServerVersionSupportingClientConst);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the executing version of the API.
|
||||
/// </summary>
|
||||
public Version Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the min client version supported by the server.
|
||||
/// </summary>
|
||||
public Version MinClientVersionSupportedByServer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the min server version supporting the client.
|
||||
/// </summary>
|
||||
public Version MinServerVersionSupportingClient { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the API server is compatible with a client.
|
||||
/// </summary>
|
||||
/// <param name="clientVersion">The client version.</param>
|
||||
/// <param name="minServerVersionSupportingClient">An opt min server version supporting the client.</param>
|
||||
/// <remarks>
|
||||
/// <para>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 .</para>
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, string> Files { get; set; }
|
||||
|
||||
public override bool IsValid => base.IsValid && Files != null;
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.ModelsBuilder.Building;
|
||||
using Umbraco.ModelsBuilder.Configuration;
|
||||
using Umbraco.Web.Mvc;
|
||||
using Umbraco.Web.WebApi;
|
||||
using Umbraco.Web.WebApi.Filters;
|
||||
using Constants = Umbraco.Core.Constants;
|
||||
|
||||
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";
|
||||
|
||||
// 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(data.Namespace, data.Files);
|
||||
|
||||
return Request.CreateResponse(HttpStatusCode.OK, models, Configuration.Formatters.JsonFormatter);
|
||||
}
|
||||
|
||||
private Attempt<HttpResponseMessage> CheckVersion(Version clientVersion, Version minServerVersionSupportingClient)
|
||||
{
|
||||
if (clientVersion == null)
|
||||
return Attempt<HttpResponseMessage>.Fail(Request.CreateResponse(HttpStatusCode.Forbidden,
|
||||
$"API version conflict: client version (<null>) 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<HttpResponseMessage>.If(isOk, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
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;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Building
|
||||
{
|
||||
// NOTE
|
||||
// The idea was to have different types of builder, because I wanted to experiment with
|
||||
// building code with CodeDom. Turns out more complicated than I thought and maybe not
|
||||
// worth it at the moment, to we're using TextBuilder and its Generate method is specific.
|
||||
//
|
||||
// Keeping the code as-is for the time being...
|
||||
|
||||
/// <summary>
|
||||
/// Provides a base class for all builders.
|
||||
/// </summary>
|
||||
internal abstract class Builder
|
||||
{
|
||||
private readonly IList<TypeModel> _typeModels;
|
||||
|
||||
protected Dictionary<string, string> ModelsMap { get; } = new Dictionary<string, string>();
|
||||
protected ParseResult ParseResult { get; }
|
||||
|
||||
// the list of assemblies that will be 'using' by default
|
||||
protected readonly IList<string> TypesUsing = new List<string>
|
||||
{
|
||||
"System",
|
||||
"System.Collections.Generic",
|
||||
"System.Linq.Expressions",
|
||||
"System.Web",
|
||||
"Umbraco.Core.Models",
|
||||
"Umbraco.Core.Models.PublishedContent",
|
||||
"Umbraco.Web",
|
||||
"Umbraco.ModelsBuilder",
|
||||
"Umbraco.ModelsBuilder.Umbraco",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the namespace to use for the models.
|
||||
/// </summary>
|
||||
/// <remarks>May be overriden by code attributes.</remarks>
|
||||
public string ModelsNamespace { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of assemblies to add to the set of 'using' assemblies in each model file.
|
||||
/// </summary>
|
||||
public IList<string> Using => TypesUsing;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of models to generate.
|
||||
/// </summary>
|
||||
/// <returns>The models to generate, ie those that are not ignored.</returns>
|
||||
public IEnumerable<TypeModel> GetModelsToGenerate()
|
||||
{
|
||||
return _typeModels.Where(x => !x.IsContentIgnored);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of all models.
|
||||
/// </summary>
|
||||
/// <remarks>Includes those that are ignored.</remarks>
|
||||
internal IList<TypeModel> TypeModels => _typeModels;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Builder"/> class with a list of models to generate
|
||||
/// and the result of code parsing.
|
||||
/// </summary>
|
||||
/// <param name="typeModels">The list of models to generate.</param>
|
||||
/// <param name="parseResult">The result of code parsing.</param>
|
||||
protected Builder(IList<TypeModel> typeModels, ParseResult parseResult)
|
||||
{
|
||||
_typeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels));
|
||||
ParseResult = parseResult ?? throw new ArgumentNullException(nameof(parseResult));
|
||||
|
||||
Prepare();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Builder"/> class with a list of models to generate,
|
||||
/// the result of code parsing, and a models namespace.
|
||||
/// </summary>
|
||||
/// <param name="typeModels">The list of models to generate.</param>
|
||||
/// <param name="parseResult">The result of code parsing.</param>
|
||||
/// <param name="modelsNamespace">The models namespace.</param>
|
||||
protected Builder(IList<TypeModel> typeModels, ParseResult parseResult, string modelsNamespace)
|
||||
: this(typeModels, parseResult)
|
||||
{
|
||||
// can be null or empty, we'll manage
|
||||
ModelsNamespace = modelsNamespace;
|
||||
}
|
||||
|
||||
// for unit tests only
|
||||
protected Builder()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Prepares generation by processing the result of code parsing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
private void Prepare()
|
||||
{
|
||||
var pureLive = UmbracoConfig.For.ModelsBuilder().ModelsMode == ModelsMode.PureLive;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// for the first two of these two tests,
|
||||
// always throw, even in purelive: cannot happen unless ppl start fidling with attributes to rename
|
||||
// things, and then they should pay attention to the generation error log - there's no magic here
|
||||
// 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))
|
||||
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))
|
||||
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 xx in typeModel.Properties.Where(x => !x.IsIgnored && x.ClrName == typeModel.ClrName))
|
||||
{
|
||||
if (!pureLive)
|
||||
throw new InvalidOperationException($"The model class for content type with alias \"{typeModel.Alias}\" is named \"{xx.ClrName}\"."
|
||||
+ $" CSharp does not support using the same name for the property with alias \"{xx.Alias}\"."
|
||||
+ " Consider using an attribute to assign a different name to the property.");
|
||||
|
||||
// for purelive, will we generate a commented out properties with an error message,
|
||||
// instead of throwing, because then it kills the sites and ppl don't understand why
|
||||
xx.AddError($"The class {typeModel.ClrName} cannot implement this property, because"
|
||||
+ $" CSharp does not support naming the property with alias \"{xx.Alias}\" with the same name as content type with alias \"{typeModel.Alias}\"."
|
||||
+ " Consider using an attribute to assign a different name to the property.");
|
||||
|
||||
// will not be implemented on interface nor class
|
||||
// note: we will still create the static getter, and implement the property on other classes...
|
||||
}
|
||||
}
|
||||
|
||||
// ensure we have no collision between base types
|
||||
// NO: we may want to define a base class in a partial, on a model that has a parent
|
||||
// we are NOT checking that the defined base type does maintain the inheritance chain
|
||||
//foreach (var xx in _typeModels.Where(x => !x.IsContentIgnored).Where(x => x.BaseType != null && x.HasBase))
|
||||
// throw new InvalidOperationException(string.Format("Type alias \"{0}\" has more than one parent class.",
|
||||
// xx.Alias));
|
||||
|
||||
// discover interfaces that need to be declared / implemented
|
||||
foreach (var typeModel in _typeModels)
|
||||
{
|
||||
// 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<TypeModel>();
|
||||
if (typeModel.BaseType != null && !typeModel.BaseType.IsContentIgnored)
|
||||
TypeModel.CollectImplems(parentImplems, typeModel.BaseType);
|
||||
|
||||
// interfaces we must declare we implement (initially empty)
|
||||
// ie this type's mixins, except those that have been removed,
|
||||
// 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);
|
||||
|
||||
// interfaces we must actually implement (initially empty)
|
||||
// if we declare we implement a mixin interface, we must actually implement
|
||||
// its properties, all recursively (ie if the mixin interface implements...)
|
||||
// so, starting with local mixins, we collect all the (non-removed) types above them
|
||||
var mixinImplems = new List<TypeModel>();
|
||||
foreach (var i in typeModel.DeclaringInterfaces)
|
||||
TypeModel.CollectImplems(mixinImplems, i);
|
||||
// and then we remove from that list anything that is already declared at the parent level
|
||||
typeModel.ImplementingInterfaces.AddRange(mixinImplems.Except(parentImplems));
|
||||
}
|
||||
|
||||
// register using types
|
||||
foreach (var usingNamespace in ParseResult.UsingNamespaces)
|
||||
{
|
||||
if (!TypesUsing.Contains(usingNamespace))
|
||||
TypesUsing.Add(usingNamespace);
|
||||
}
|
||||
|
||||
// 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<string, string> { { "code", codeBuilder.ToString() } }, out trees);
|
||||
var tree = trees[0];
|
||||
_ambiguousSymbolsModel = compilation.GetSemanticModel(tree);
|
||||
|
||||
var namespaceSyntax = tree.GetRoot().DescendantNodes().OfType<NamespaceDeclarationSyntax>().First();
|
||||
//var namespaceSymbol = model.GetDeclaredSymbol(namespaceSyntax);
|
||||
_ambiguousSymbolsPos = namespaceSyntax.OpenBraceToken.SpanStart;
|
||||
}
|
||||
|
||||
// looking for a simple symbol eg 'Umbraco' or 'String'
|
||||
// expecting to match eg 'Umbraco' or 'System.String'
|
||||
// returns true if either
|
||||
// - more than 1 symbol is found (explicitely ambiguous)
|
||||
// - 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);
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
internal string ModelsNamespaceForTests;
|
||||
|
||||
public string GetModelsNamespace()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
protected string GetModelsBaseClassName(TypeModel type)
|
||||
{
|
||||
// code attribute overrides everything
|
||||
if (ParseResult.HasModelsBaseClassName)
|
||||
return ParseResult.ModelsBaseClassName;
|
||||
|
||||
// default
|
||||
return type.IsElement ? "PublishedElementModel" : "PublishedContentModel";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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...
|
||||
|
||||
/// <summary>
|
||||
/// Implements a builder that works by using CodeDom
|
||||
/// </summary>
|
||||
internal class CodeDomBuilder : Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CodeDomBuilder"/> class with a list of models to generate.
|
||||
/// </summary>
|
||||
/// <param name="typeModels">The list of models to generate.</param>
|
||||
public CodeDomBuilder(IList<TypeModel> typeModels)
|
||||
: base(typeModels, null)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Outputs a generated model to a code namespace.
|
||||
/// </summary>
|
||||
/// <param name="ns">The code namespace.</param>
|
||||
/// <param name="typeModel">The model to generate.</param>
|
||||
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[] // <T>
|
||||
{
|
||||
new CodeTypeReference(propertyModel.ModelClrType)
|
||||
}),
|
||||
new CodeExpression[] // ("alias")
|
||||
{
|
||||
new CodePrimitiveExpression(propertyModel.Alias)
|
||||
})));
|
||||
c.Members.Add(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements code parsing.
|
||||
/// </summary>
|
||||
/// <remarks>Parses user's code and look for generator's instructions.</remarks>
|
||||
internal class CodeParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a set of file.
|
||||
/// </summary>
|
||||
/// <param name="files">A set of (filename,content) representing content to parse.</param>
|
||||
/// <returns>The result of the code parsing.</returns>
|
||||
/// <remarks>The set of files is a dictionary of name, content.</remarks>
|
||||
public ParseResult Parse(IDictionary<string, string> files)
|
||||
{
|
||||
return Parse(files, Enumerable.Empty<PortableExecutableReference>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a set of file.
|
||||
/// </summary>
|
||||
/// <param name="files">A set of (filename,content) representing content to parse.</param>
|
||||
/// <param name="references">Assemblies to reference in compilations.</param>
|
||||
/// <returns>The result of the code parsing.</returns>
|
||||
/// <remarks>The set of files is a dictionary of name, content.</remarks>
|
||||
public ParseResult Parse(IDictionary<string, string> files, IEnumerable<PortableExecutableReference> 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<string, string> 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<ClassDeclarationSyntax>();
|
||||
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<InterfaceDeclarationSyntax>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<PortableExecutableReference> References { get; set; }
|
||||
|
||||
public bool Debug { get; set; }
|
||||
|
||||
// gets a compilation
|
||||
public CSharpCompilation GetCompilation(string assemblyName, IDictionary<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> { { path, code } }, stream);
|
||||
return Assembly.Load(stream.GetBuffer());
|
||||
}
|
||||
}
|
||||
|
||||
// compiles files into a stream
|
||||
public void Compile(string assemblyName, IDictionary<string, string> 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<string, string> { { path, code } }, stream);
|
||||
}
|
||||
|
||||
private static void ThrowExceptionFromDiagnostic(IDictionary<string, string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Building
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the result of a code parsing.
|
||||
/// </summary>
|
||||
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<string> _ignoredContent
|
||||
= new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
|
||||
//private readonly HashSet<string> _ignoredMixin
|
||||
// = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
|
||||
//private readonly HashSet<string> _ignoredMixinProperties
|
||||
// = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
|
||||
private readonly Dictionary<string, string> _renamedContent
|
||||
= new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
|
||||
private readonly HashSet<string> _withImplementContent
|
||||
= new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
|
||||
private readonly Dictionary<string, HashSet<string>> _ignoredProperty
|
||||
= new Dictionary<string, HashSet<string>>();
|
||||
private readonly Dictionary<string, Dictionary<string, string>> _renamedProperty
|
||||
= new Dictionary<string, Dictionary<string, string>>();
|
||||
private readonly Dictionary<string, string> _contentBase
|
||||
= new Dictionary<string, string>();
|
||||
private readonly Dictionary<string, string[]> _contentInterfaces
|
||||
= new Dictionary<string, string[]>();
|
||||
private readonly List<string> _usingNamespaces
|
||||
= new List<string>();
|
||||
private readonly Dictionary<string, List<StaticMixinMethodInfo>> _staticMixins
|
||||
= new Dictionary<string, List<StaticMixinMethodInfo>>();
|
||||
private readonly HashSet<string> _withCtor
|
||||
= new HashSet<string>(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<string> ignores;
|
||||
if (!_ignoredProperty.TryGetValue(contentName, out ignores))
|
||||
ignores = _ignoredProperty[contentName] = new HashSet<string>(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<string, string> renames;
|
||||
if (!_renamedProperty.TryGetValue(contentName, out renames))
|
||||
renames = _renamedProperty[contentName] = new Dictionary<string, string>(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<string> 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<StaticMixinMethodInfo>();
|
||||
|
||||
_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<string> 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<string> 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<string, string> 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<string> UsingNamespaces
|
||||
{
|
||||
get { return _usingNamespaces; }
|
||||
}
|
||||
|
||||
public IEnumerable<string> StaticMixinMethods(string contentName)
|
||||
{
|
||||
return _staticMixins.ContainsKey(contentName)
|
||||
? _staticMixins[contentName].Select(x => x.MethodName)
|
||||
: Enumerable.Empty<string>() ;
|
||||
}
|
||||
|
||||
public bool HasCtor(string contentName)
|
||||
{
|
||||
return _withCtor.Contains(contentName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Building
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a model property.
|
||||
/// </summary>
|
||||
public class PropertyModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the alias of the property.
|
||||
/// </summary>
|
||||
public string Alias;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the property.
|
||||
/// </summary>
|
||||
public string Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of the property.
|
||||
/// </summary>
|
||||
public string Description;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the clr name of the property.
|
||||
/// </summary>
|
||||
/// <remarks>This is just the local name eg "Price".</remarks>
|
||||
public string ClrName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Model Clr type of the property values.
|
||||
/// </summary>
|
||||
/// <remarks>As indicated by the <c>PublishedPropertyType</c>, ie by the <c>IPropertyValueConverter</c>
|
||||
/// if any, else <c>object</c>. May include some ModelType that will need to be mapped.</remarks>
|
||||
public Type ModelClrType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the CLR type name of the property values.
|
||||
/// </summary>
|
||||
public string ClrTypeName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this property should be excluded from generation.
|
||||
/// </summary>
|
||||
public bool IsIgnored;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the generation errors for the property.
|
||||
/// </summary>
|
||||
/// <remarks>This should be null, unless something prevents the property from being
|
||||
/// generated, and then the value should explain what. This can be used to generate
|
||||
/// commented out code eg in PureLive.</remarks>
|
||||
public List<string> Errors;
|
||||
|
||||
/// <summary>
|
||||
/// Adds an error.
|
||||
/// </summary>
|
||||
public void AddError(string error)
|
||||
{
|
||||
if (Errors == null) Errors = new List<string>();
|
||||
Errors.Add(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,554 +0,0 @@
|
||||
using System;
|
||||
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;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Building
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a builder that works by writing text.
|
||||
/// </summary>
|
||||
internal class TextBuilder : Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextBuilder"/> class with a list of models to generate
|
||||
/// and the result of code parsing.
|
||||
/// </summary>
|
||||
/// <param name="typeModels">The list of models to generate.</param>
|
||||
/// <param name="parseResult">The result of code parsing.</param>
|
||||
public TextBuilder(IList<TypeModel> typeModels, ParseResult parseResult)
|
||||
: base(typeModels, parseResult)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextBuilder"/> class with a list of models to generate,
|
||||
/// the result of code parsing, and a models namespace.
|
||||
/// </summary>
|
||||
/// <param name="typeModels">The list of models to generate.</param>
|
||||
/// <param name="parseResult">The result of code parsing.</param>
|
||||
/// <param name="modelsNamespace">The models namespace.</param>
|
||||
public TextBuilder(IList<TypeModel> typeModels, ParseResult parseResult, string modelsNamespace)
|
||||
: base(typeModels, parseResult, modelsNamespace)
|
||||
{ }
|
||||
|
||||
// internal for unit tests only
|
||||
internal TextBuilder()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Outputs a generated model to a string builder.
|
||||
/// </summary>
|
||||
/// <param name="sb">The string builder.</param>
|
||||
/// <param name="typeModel">The model to generate.</param>
|
||||
public void Generate(StringBuilder sb, TypeModel typeModel)
|
||||
{
|
||||
WriteHeader(sb);
|
||||
|
||||
foreach (var t in TypesUsing)
|
||||
sb.AppendFormat("using {0};\n", t);
|
||||
|
||||
sb.Append("\n");
|
||||
sb.AppendFormat("namespace {0}\n", GetModelsNamespace());
|
||||
sb.Append("{\n");
|
||||
|
||||
WriteContentType(sb, typeModel);
|
||||
|
||||
sb.Append("}\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Outputs generated models to a string builder.
|
||||
/// </summary>
|
||||
/// <param name="sb">The string builder.</param>
|
||||
/// <param name="typeModels">The models to generate.</param>
|
||||
public void Generate(StringBuilder sb, IEnumerable<TypeModel> typeModels)
|
||||
{
|
||||
WriteHeader(sb);
|
||||
|
||||
foreach (var t in TypesUsing)
|
||||
sb.AppendFormat("using {0};\n", t);
|
||||
|
||||
// assembly attributes marker
|
||||
sb.Append("\n//ASSATTR\n");
|
||||
|
||||
sb.Append("\n");
|
||||
sb.AppendFormat("namespace {0}\n", GetModelsNamespace());
|
||||
sb.Append("{\n");
|
||||
|
||||
foreach (var typeModel in typeModels)
|
||||
{
|
||||
WriteContentType(sb, typeModel);
|
||||
sb.Append("\n");
|
||||
}
|
||||
|
||||
sb.Append("}\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Outputs an "auto-generated" header to a string builder.
|
||||
/// </summary>
|
||||
/// <param name="sb">The string builder.</param>
|
||||
public static void WriteHeader(StringBuilder sb)
|
||||
{
|
||||
TextHeaderWriter.WriteHeader(sb);
|
||||
}
|
||||
|
||||
private void WriteContentType(StringBuilder sb, TypeModel type)
|
||||
{
|
||||
string sep;
|
||||
|
||||
if (type.IsMixin)
|
||||
{
|
||||
// write the interface declaration
|
||||
sb.AppendFormat("\t// Mixin content Type {0} with alias \"{1}\"\n", type.Id, type.Alias);
|
||||
if (!string.IsNullOrWhiteSpace(type.Name))
|
||||
sb.AppendFormat("\t/// <summary>{0}</summary>\n", XmlCommentString(type.Name));
|
||||
sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName);
|
||||
var implements = type.BaseType == null || type.BaseType.IsContentIgnored
|
||||
? (type.HasBase ? null : (type.IsElement ? "PublishedElement" : "PublishedContent"))
|
||||
: type.BaseType.ClrName;
|
||||
if (implements != null)
|
||||
sb.AppendFormat(" : I{0}", implements);
|
||||
|
||||
// write the mixins
|
||||
sep = implements == null ? ":" : ",";
|
||||
foreach (var mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName))
|
||||
{
|
||||
sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName);
|
||||
sep = ",";
|
||||
}
|
||||
|
||||
sb.Append("\n\t{\n");
|
||||
|
||||
// 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))
|
||||
{
|
||||
if (more) sb.Append("\n");
|
||||
more = true;
|
||||
WriteInterfaceProperty(sb, prop);
|
||||
}
|
||||
|
||||
sb.Append("\t}\n\n");
|
||||
}
|
||||
|
||||
// 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/// <summary>{0}</summary>\n", XmlCommentString(type.Name));
|
||||
// cannot do it now. see note in ImplementContentTypeAttribute
|
||||
//if (!type.HasImplement)
|
||||
// sb.AppendFormat("\t[ImplementContentType(\"{0}\")]\n", type.Alias);
|
||||
sb.AppendFormat("\t[PublishedModel(\"{0}\")]\n", type.Alias);
|
||||
sb.AppendFormat("\tpublic partial class {0}", type.ClrName);
|
||||
var inherits = type.HasBase
|
||||
? null // has its own base already
|
||||
: (type.BaseType == null || type.BaseType.IsContentIgnored
|
||||
? GetModelsBaseClassName(type)
|
||||
: type.BaseType.ClrName);
|
||||
if (inherits != null)
|
||||
sb.AppendFormat(" : {0}", inherits);
|
||||
|
||||
sep = inherits == null ? ":" : ",";
|
||||
if (type.IsMixin)
|
||||
{
|
||||
// if it's a mixin it implements its own interface
|
||||
sb.AppendFormat("{0} I{1}", sep, type.ClrName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// write the mixins, if any, as interfaces
|
||||
// only if not a mixin because otherwise the interface already has them already
|
||||
foreach (var mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName))
|
||||
{
|
||||
sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName);
|
||||
sep = ",";
|
||||
}
|
||||
}
|
||||
|
||||
// begin class body
|
||||
sb.Append("\n\t{\n");
|
||||
|
||||
// write the constants & static methods
|
||||
// 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");
|
||||
sb.AppendFormat("\t\tpublic new const string ModelTypeAlias = \"{0}\";\n",
|
||||
type.Alias);
|
||||
var itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme
|
||||
sb.AppendFormat("\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n",
|
||||
itemType);
|
||||
sb.Append("\t\tpublic new static PublishedContentType GetModelContentType()\n");
|
||||
sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias);\n");
|
||||
sb.AppendFormat("\t\tpublic static PublishedPropertyType GetModelPropertyType<TValue>(Expression<Func<{0}, TValue>> 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");
|
||||
|
||||
// write the properties
|
||||
sb.Append("\t\t// properties\n");
|
||||
WriteContentTypeProperties(sb, type);
|
||||
|
||||
// close the class declaration
|
||||
sb.Append("\t}\n");
|
||||
}
|
||||
|
||||
private void WriteContentTypeProperties(StringBuilder sb, TypeModel type)
|
||||
{
|
||||
var staticMixinGetters = UmbracoConfig.For.ModelsBuilder().StaticMixinGetters;
|
||||
|
||||
// write the properties
|
||||
foreach (var prop in type.Properties.Where(x => !x.IsIgnored).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
|
||||
// and the parent defines its own properties. need to write the mixins properties
|
||||
// since the mixins are only interfaces and we have to provide an implementation.
|
||||
|
||||
// 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))
|
||||
if (staticMixinGetters)
|
||||
WriteMixinProperty(sb, prop, mixinType.ClrName);
|
||||
else
|
||||
WriteProperty(sb, mixinType, prop);
|
||||
}
|
||||
|
||||
private void WriteMixinProperty(StringBuilder sb, PropertyModel property, string mixinClrName)
|
||||
{
|
||||
sb.Append("\n");
|
||||
|
||||
// Adds xml summary to each property containing
|
||||
// property name and property description
|
||||
if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description))
|
||||
{
|
||||
sb.Append("\t\t///<summary>\n");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(property.Description))
|
||||
sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), XmlCommentString(property.Description));
|
||||
else
|
||||
sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name));
|
||||
|
||||
sb.Append("\t\t///</summary>\n");
|
||||
}
|
||||
|
||||
sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias);
|
||||
|
||||
sb.Append("\t\tpublic ");
|
||||
WriteClrType(sb, property.ClrTypeName);
|
||||
|
||||
sb.AppendFormat(" {0} => ",
|
||||
property.ClrName);
|
||||
WriteNonGenericClrType(sb, GetModelsNamespace() + "." + mixinClrName);
|
||||
sb.AppendFormat(".{0}(this);\n",
|
||||
MixinStaticGetterName(property.ClrName));
|
||||
}
|
||||
|
||||
private static string MixinStaticGetterName(string clrName)
|
||||
{
|
||||
return string.Format(UmbracoConfig.For.ModelsBuilder().StaticMixinGetterPattern, clrName);
|
||||
}
|
||||
|
||||
private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string mixinClrName = null)
|
||||
{
|
||||
var mixinStatic = mixinClrName != null;
|
||||
|
||||
sb.Append("\n");
|
||||
|
||||
if (property.Errors != null)
|
||||
{
|
||||
sb.Append("\t\t/*\n");
|
||||
sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n");
|
||||
sb.Append("\t\t *\n");
|
||||
var first = true;
|
||||
foreach (var error in property.Errors)
|
||||
{
|
||||
if (first) first = false;
|
||||
else sb.Append("\t\t *\n");
|
||||
foreach (var s in SplitError(error))
|
||||
{
|
||||
sb.Append("\t\t * ");
|
||||
sb.Append(s);
|
||||
sb.Append("\n");
|
||||
}
|
||||
}
|
||||
sb.Append("\t\t *\n");
|
||||
sb.Append("\n");
|
||||
}
|
||||
|
||||
// Adds xml summary to each property containing
|
||||
// property name and property description
|
||||
if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description))
|
||||
{
|
||||
sb.Append("\t\t///<summary>\n");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(property.Description))
|
||||
sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), XmlCommentString(property.Description));
|
||||
else
|
||||
sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name));
|
||||
|
||||
sb.Append("\t\t///</summary>\n");
|
||||
}
|
||||
|
||||
sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias);
|
||||
|
||||
if (mixinStatic)
|
||||
{
|
||||
sb.Append("\t\tpublic ");
|
||||
WriteClrType(sb, property.ClrTypeName);
|
||||
sb.AppendFormat(" {0} => {1}(this);\n",
|
||||
property.ClrName, MixinStaticGetterName(property.ClrName));
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append("\t\tpublic ");
|
||||
WriteClrType(sb, property.ClrTypeName);
|
||||
sb.AppendFormat(" {0} => this.Value",
|
||||
property.ClrName);
|
||||
if (property.ModelClrType != typeof(object))
|
||||
{
|
||||
sb.Append("<");
|
||||
WriteClrType(sb, property.ClrTypeName);
|
||||
sb.Append(">");
|
||||
}
|
||||
sb.AppendFormat("(\"{0}\");\n",
|
||||
property.Alias);
|
||||
}
|
||||
|
||||
if (property.Errors != null)
|
||||
{
|
||||
sb.Append("\n");
|
||||
sb.Append("\t\t *\n");
|
||||
sb.Append("\t\t */\n");
|
||||
}
|
||||
|
||||
if (!mixinStatic) return;
|
||||
|
||||
var mixinStaticGetterName = MixinStaticGetterName(property.ClrName);
|
||||
|
||||
if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return;
|
||||
|
||||
sb.Append("\n");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(property.Name))
|
||||
sb.AppendFormat("\t\t/// <summary>Static getter for {0}</summary>\n", XmlCommentString(property.Name));
|
||||
|
||||
sb.Append("\t\tpublic static ");
|
||||
WriteClrType(sb, property.ClrTypeName);
|
||||
sb.AppendFormat(" {0}(I{1} that) => that.Value",
|
||||
mixinStaticGetterName, mixinClrName);
|
||||
if (property.ModelClrType != typeof(object))
|
||||
{
|
||||
sb.Append("<");
|
||||
WriteClrType(sb, property.ClrTypeName);
|
||||
sb.Append(">");
|
||||
}
|
||||
sb.AppendFormat("(\"{0}\");\n",
|
||||
property.Alias);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> SplitError(string error)
|
||||
{
|
||||
var p = 0;
|
||||
while (p < error.Length)
|
||||
{
|
||||
var n = p + 50;
|
||||
while (n < error.Length && error[n] != ' ') n++;
|
||||
if (n >= error.Length) break;
|
||||
yield return error.Substring(p, n - p);
|
||||
p = n + 1;
|
||||
}
|
||||
if (p < error.Length)
|
||||
yield return error.Substring(p);
|
||||
}
|
||||
|
||||
private void WriteInterfaceProperty(StringBuilder sb, PropertyModel property)
|
||||
{
|
||||
if (property.Errors != null)
|
||||
{
|
||||
sb.Append("\t\t/*\n");
|
||||
sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n");
|
||||
sb.Append("\t\t *\n");
|
||||
var first = true;
|
||||
foreach (var error in property.Errors)
|
||||
{
|
||||
if (first) first = false;
|
||||
else sb.Append("\t\t *\n");
|
||||
foreach (var s in SplitError(error))
|
||||
{
|
||||
sb.Append("\t\t * ");
|
||||
sb.Append(s);
|
||||
sb.Append("\n");
|
||||
}
|
||||
}
|
||||
sb.Append("\t\t *\n");
|
||||
sb.Append("\n");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(property.Name))
|
||||
sb.AppendFormat("\t\t/// <summary>{0}</summary>\n", XmlCommentString(property.Name));
|
||||
sb.Append("\t\t");
|
||||
WriteClrType(sb, property.ClrTypeName);
|
||||
sb.AppendFormat(" {0} {{ get; }}\n",
|
||||
property.ClrName);
|
||||
|
||||
if (property.Errors != null)
|
||||
{
|
||||
sb.Append("\n");
|
||||
sb.Append("\t\t *\n");
|
||||
sb.Append("\t\t */\n");
|
||||
}
|
||||
}
|
||||
|
||||
// internal for unit tests
|
||||
internal void WriteClrType(StringBuilder sb, Type type)
|
||||
{
|
||||
var s = type.ToString();
|
||||
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var p = s.IndexOf('`');
|
||||
WriteNonGenericClrType(sb, s.Substring(0, p));
|
||||
sb.Append("<");
|
||||
var args = type.GetGenericArguments();
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(", ");
|
||||
WriteClrType(sb, args[i]);
|
||||
}
|
||||
sb.Append(">");
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteNonGenericClrType(sb, s);
|
||||
}
|
||||
}
|
||||
|
||||
internal void WriteClrType(StringBuilder sb, string type)
|
||||
{
|
||||
var p = type.IndexOf('<');
|
||||
if (type.Contains('<'))
|
||||
{
|
||||
WriteNonGenericClrType(sb, type.Substring(0, p));
|
||||
sb.Append("<");
|
||||
var args = type.Substring(p + 1).TrimEnd('>').Split(','); // fixme will NOT work with nested generic types
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(", ");
|
||||
WriteClrType(sb, args[i]);
|
||||
}
|
||||
sb.Append(">");
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteNonGenericClrType(sb, type);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteNonGenericClrType(StringBuilder sb, string s)
|
||||
{
|
||||
// map model types
|
||||
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))
|
||||
{
|
||||
sb.Append(typeName);
|
||||
return;
|
||||
}
|
||||
|
||||
// if full type name matches a using clause, strip
|
||||
// so if we want Umbraco.Core.Models.IPublishedContent
|
||||
// and using Umbraco.Core.Models, then we just need IPublishedContent
|
||||
typeName = s;
|
||||
string typeUsing = null;
|
||||
var p = typeName.LastIndexOf('.');
|
||||
if (p > 0)
|
||||
{
|
||||
var x = typeName.Substring(0, p);
|
||||
if (Using.Contains(x))
|
||||
{
|
||||
typeName = typeName.Substring(p + 1);
|
||||
typeUsing = x;
|
||||
}
|
||||
}
|
||||
|
||||
// nested types *after* using
|
||||
typeName = typeName.Replace("+", ".");
|
||||
|
||||
// symbol to test is the first part of the name
|
||||
// so if type name is Foo.Bar.Nil we want to ensure that Foo is not ambiguous
|
||||
p = typeName.IndexOf('.');
|
||||
var symbol = p > 0 ? typeName.Substring(0, p) : typeName;
|
||||
|
||||
// what we should find - WITHOUT any generic <T> thing - just the type
|
||||
// no 'using' = the exact symbol
|
||||
// a 'using' = using.symbol
|
||||
var match = typeUsing == null ? symbol : (typeUsing + "." + symbol);
|
||||
|
||||
// if not ambiguous, be happy
|
||||
if (!IsAmbiguousSymbol(symbol, match))
|
||||
{
|
||||
sb.Append(typeName);
|
||||
return;
|
||||
}
|
||||
|
||||
// symbol is ambiguous
|
||||
// if no 'using', must prepend global::
|
||||
if (typeUsing == null)
|
||||
{
|
||||
sb.Append("global::");
|
||||
sb.Append(s.Replace("+", "."));
|
||||
return;
|
||||
}
|
||||
|
||||
// could fullname be non-ambiguous?
|
||||
// note: all-or-nothing, not trying to segment the using clause
|
||||
typeName = s.Replace("+", ".");
|
||||
p = typeName.IndexOf('.');
|
||||
symbol = typeName.Substring(0, p);
|
||||
match = symbol;
|
||||
|
||||
// still ambiguous, must prepend global::
|
||||
if (IsAmbiguousSymbol(symbol, match))
|
||||
sb.Append("global::");
|
||||
|
||||
sb.Append(typeName);
|
||||
}
|
||||
|
||||
private static string XmlCommentString(string s)
|
||||
{
|
||||
return s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' ');
|
||||
}
|
||||
|
||||
private static readonly IDictionary<string, string> TypesMap = new Dictionary<string, string>
|
||||
{
|
||||
{ "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" }
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System.Text;
|
||||
using Umbraco.ModelsBuilder.Api;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Building
|
||||
{
|
||||
public static class TextHeaderWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Outputs an "auto-generated" header to a string builder.
|
||||
/// </summary>
|
||||
/// <param name="sb">The string builder.</param>
|
||||
public static void WriteHeader(StringBuilder sb)
|
||||
{
|
||||
sb.Append("//------------------------------------------------------------------------------\n");
|
||||
sb.Append("// <auto-generated>\n");
|
||||
sb.Append("// This code was generated by a tool.\n");
|
||||
sb.Append("//\n");
|
||||
sb.AppendFormat("// Umbraco.ModelsBuilder v{0}\n", ApiVersion.Current.Version);
|
||||
sb.Append("//\n");
|
||||
sb.Append("// Changes to this file will be lost if the code is regenerated.\n");
|
||||
sb.Append("// </auto-generated>\n");
|
||||
sb.Append("//------------------------------------------------------------------------------\n");
|
||||
sb.Append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Building
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a model.
|
||||
/// </summary>
|
||||
public class TypeModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the corresponding content type.
|
||||
/// </summary>
|
||||
public int Id;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the alias of the model.
|
||||
/// </summary>
|
||||
public string Alias;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the content type.
|
||||
/// </summary>
|
||||
public string Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of the content type.
|
||||
/// </summary>
|
||||
public string Description;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the clr name of the model.
|
||||
/// </summary>
|
||||
/// <remarks>This is the complete name eg "Foo.Bar.MyContent".</remarks>
|
||||
public string ClrName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the parent.
|
||||
/// </summary>
|
||||
/// <remarks>The parent can either be a base content type, or a content types container. If the content
|
||||
/// type does not have a base content type, then returns <c>-1</c>.</remarks>
|
||||
public int ParentId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base model.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>If the content type does not have a base content type, then returns <c>null</c>.</para>
|
||||
/// <para>The current model inherits from its base model.</para>
|
||||
/// </remarks>
|
||||
public TypeModel BaseType; // the parent type in Umbraco (type inherits its properties)
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of properties that are defined by this model.
|
||||
/// </summary>
|
||||
/// <remarks>These are only those property that are defined locally by this model,
|
||||
/// and the list does not contain properties inherited from base models or from mixins.</remarks>
|
||||
public readonly List<PropertyModel> Properties = new List<PropertyModel>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mixin models.
|
||||
/// </summary>
|
||||
/// <remarks>The current model implements mixins.</remarks>
|
||||
public readonly List<TypeModel> MixinTypes = new List<TypeModel>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of interfaces that this model needs to declare it implements.
|
||||
/// </summary>
|
||||
/// <remarks>Some of these interfaces may actually be implemented by a base model
|
||||
/// that this model inherits from.</remarks>
|
||||
public readonly List<TypeModel> DeclaringInterfaces = new List<TypeModel>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of interfaces that this model needs to actually implement.
|
||||
/// </summary>
|
||||
public readonly List<TypeModel> ImplementingInterfaces = new List<TypeModel>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of existing static mixin method candidates.
|
||||
/// </summary>
|
||||
public readonly List<string> StaticMixinMethods = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this model has a base class.
|
||||
/// </summary>
|
||||
/// <remarks>Can be either because the content type has a base content type declared in Umbraco,
|
||||
/// or because the existing user's code declares a base class for this model.</remarks>
|
||||
public bool HasBase;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this model has been renamed.
|
||||
/// </summary>
|
||||
public bool IsRenamed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this model has [ImplementContentType] already.
|
||||
/// </summary>
|
||||
public bool HasImplement;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this model is used as a mixin by another model.
|
||||
/// </summary>
|
||||
public bool IsMixin;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this model is the base model of another model.
|
||||
/// </summary>
|
||||
public bool IsParent;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this model should be excluded from generation.
|
||||
/// </summary>
|
||||
public bool IsContentIgnored;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the ctor is already defined in a partial.
|
||||
/// </summary>
|
||||
public bool HasCtor;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the type is an element.
|
||||
/// </summary>
|
||||
public bool IsElement => ItemType == ItemTypes.Element;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the different model item types.
|
||||
/// </summary>
|
||||
public enum ItemTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// Element.
|
||||
/// </summary>
|
||||
Element,
|
||||
|
||||
/// <summary>
|
||||
/// Content.
|
||||
/// </summary>
|
||||
Content,
|
||||
|
||||
/// <summary>
|
||||
/// Media.
|
||||
/// </summary>
|
||||
Media,
|
||||
|
||||
/// <summary>
|
||||
/// Member.
|
||||
/// </summary>
|
||||
Member
|
||||
}
|
||||
|
||||
private ItemTypes _itemType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the model item type.
|
||||
/// </summary>
|
||||
public ItemTypes ItemType
|
||||
{
|
||||
get { return _itemType; }
|
||||
set
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case ItemTypes.Element:
|
||||
case ItemTypes.Content:
|
||||
case ItemTypes.Media:
|
||||
case ItemTypes.Member:
|
||||
_itemType = value;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("value");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively collects all types inherited, or implemented as interfaces, by a specified type.
|
||||
/// </summary>
|
||||
/// <param name="types">The collection.</param>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <remarks>Includes the specified type.</remarks>
|
||||
internal static void CollectImplems(ICollection<TypeModel> types, TypeModel type)
|
||||
{
|
||||
if (!type.IsContentIgnored && types.Contains(type) == false)
|
||||
types.Add(type);
|
||||
if (type.BaseType != null && !type.BaseType.IsContentIgnored)
|
||||
CollectImplems(types, type.BaseType);
|
||||
foreach (var mixin in type.MixinTypes.Where(x => !x.IsContentIgnored))
|
||||
CollectImplems(types, mixin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the base models starting from the current model up.
|
||||
/// </summary>
|
||||
/// <param name="andSelf">Indicates whether the enumeration should start with the current model
|
||||
/// or from its base model.</param>
|
||||
/// <returns>The base models.</returns>
|
||||
public IEnumerable<TypeModel> EnumerateBaseTypes(bool andSelf = false)
|
||||
{
|
||||
var typeModel = andSelf ? this : BaseType;
|
||||
while (typeModel != null)
|
||||
{
|
||||
yield return typeModel;
|
||||
typeModel = typeModel.BaseType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
namespace Umbraco.ModelsBuilder.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the CLR name sources.
|
||||
/// </summary>
|
||||
public enum ClrNameSource
|
||||
{
|
||||
/// <summary>
|
||||
/// No source.
|
||||
/// </summary>
|
||||
Nothing = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Use the name as source.
|
||||
/// </summary>
|
||||
Name,
|
||||
|
||||
/// <summary>
|
||||
/// Use the alias as source.
|
||||
/// </summary>
|
||||
Alias,
|
||||
|
||||
/// <summary>
|
||||
/// Use the alias directly.
|
||||
/// </summary>
|
||||
RawAlias
|
||||
}
|
||||
}
|
||||
@@ -1,357 +0,0 @@
|
||||
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;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the models builder configuration.
|
||||
/// </summary>
|
||||
public class Config
|
||||
{
|
||||
private static Config _value;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal static Config Value => _value ?? new Config();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the configuration programmatically.
|
||||
/// </summary>
|
||||
/// <param name="config">The configuration.</param>
|
||||
/// <remarks>
|
||||
/// <para>Once the configuration has been accessed via the UmbracoConfig extension,
|
||||
/// it cannot be changed anymore, and using this method will achieve nothing.</para>
|
||||
/// <para>For tests, see UmbracoConfigExtensions.ResetConfig().</para>
|
||||
/// </remarks>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Config"/> class.
|
||||
/// </summary>
|
||||
private Config()
|
||||
{
|
||||
const string prefix = "Umbraco.ModelsBuilder.";
|
||||
|
||||
// giant kill switch, default: false
|
||||
// must be explicitely set to true for anything else to happen
|
||||
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("~/");
|
||||
DebugLevel = 0;
|
||||
|
||||
// stop here, everything is false
|
||||
if (!Enable) return;
|
||||
|
||||
// mode
|
||||
var modelsMode = ConfigurationManager.AppSettings[prefix + "ModelsMode"];
|
||||
if (!string.IsNullOrWhiteSpace(modelsMode))
|
||||
{
|
||||
switch (modelsMode)
|
||||
{
|
||||
case nameof(ModelsMode.Nothing):
|
||||
ModelsMode = ModelsMode.Nothing;
|
||||
break;
|
||||
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;
|
||||
case nameof(ModelsMode.LiveAppData):
|
||||
ModelsMode = ModelsMode.LiveAppData;
|
||||
break;
|
||||
default:
|
||||
throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode."
|
||||
+ " Note that modes are case-sensitive.");
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
var value = ConfigurationManager.AppSettings[prefix + "ModelsNamespace"];
|
||||
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();
|
||||
if (root == null)
|
||||
throw new ConfigurationErrorsException("Could not determine root directory.");
|
||||
|
||||
// GetModelsDirectory will ensure that the path is safe
|
||||
ModelsDirectory = GetModelsDirectory(root, value, AcceptUnsafeModelsDirectory);
|
||||
}
|
||||
|
||||
// default: 0
|
||||
value = ConfigurationManager.AppSettings[prefix + "DebugLevel"];
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
int debugLevel;
|
||||
if (!int.TryParse(value, out debugLevel))
|
||||
throw new ConfigurationErrorsException($"Invalid debug level \"{value}\".");
|
||||
DebugLevel = debugLevel;
|
||||
}
|
||||
|
||||
// not flagging if not generating, or live (incl. pure)
|
||||
if (ModelsMode == ModelsMode.Nothing || ModelsMode.IsLive())
|
||||
FlagOutOfDateModels = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Config"/> class.
|
||||
/// </summary>
|
||||
public Config(
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// internal for tests
|
||||
internal static string GetModelsDirectory(string root, string config, bool acceptUnsafe)
|
||||
{
|
||||
// making sure it is safe, ie under the website root,
|
||||
// unless AcceptUnsafeModelsDirectory and then everything is OK.
|
||||
|
||||
if (!Path.IsPathRooted(root))
|
||||
throw new ConfigurationErrorsException($"Root is not rooted \"{root}\".");
|
||||
|
||||
if (config.StartsWith("~/"))
|
||||
{
|
||||
var dir = Path.Combine(root, config.TrimStart("~/"));
|
||||
|
||||
// sanitize - GetFullPath will take care of any relative
|
||||
// segments in path, eg '../../foo.tmp' - it may throw a SecurityException
|
||||
// if the combined path reaches illegal parts of the filesystem
|
||||
dir = Path.GetFullPath(dir);
|
||||
root = Path.GetFullPath(root);
|
||||
|
||||
if (!dir.StartsWith(root) && !acceptUnsafe)
|
||||
throw new ConfigurationErrorsException($"Invalid models directory \"{config}\".");
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
if (acceptUnsafe)
|
||||
return Path.GetFullPath(config);
|
||||
|
||||
throw new ConfigurationErrorsException($"Invalid models directory \"{config}\".");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the whole models experience is enabled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>If this is false then absolutely nothing happens.</para>
|
||||
/// <para>Default value is <c>false</c> which means that unless we have this setting, nothing happens.</para>
|
||||
/// </remarks>
|
||||
public bool Enable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the models mode.
|
||||
/// </summary>
|
||||
public ModelsMode ModelsMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to serve the API.
|
||||
/// </summary>
|
||||
public bool ApiServer => EnableApi && ApiInstalled && IsDebug;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to enable the API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Default value is <c>true</c>.</para>
|
||||
/// <para>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.</para>
|
||||
/// </remarks>
|
||||
public bool EnableApi { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the API is installed.
|
||||
/// </summary>
|
||||
// fixme - this is now always true as the API is part of Core
|
||||
public bool ApiInstalled => true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether system.web/compilation/@debug is true.
|
||||
/// </summary>
|
||||
public bool IsDebug
|
||||
{
|
||||
get
|
||||
{
|
||||
var section = (CompilationSection) ConfigurationManager.GetSection("system.web/compilation");
|
||||
return section != null && section.Debug;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the models namespace.
|
||||
/// </summary>
|
||||
/// <remarks>That value could be overriden by other (attribute in user's code...). Return default if no value was supplied.</remarks>
|
||||
public string ModelsNamespace { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether we should enable the models factory.
|
||||
/// </summary>
|
||||
/// <remarks>Default value is <c>true</c> because no factory is enabled by default in Umbraco.</remarks>
|
||||
public bool EnableFactory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Roslyn parser language version.
|
||||
/// </summary>
|
||||
/// <remarks>Default value is <c>CSharp6</c>.</remarks>
|
||||
public LanguageVersion LanguageVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to generate static mixin getters.
|
||||
/// </summary>
|
||||
/// <remarks>Default value is <c>false</c> for backward compat reaons.</remarks>
|
||||
public bool StaticMixinGetters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string pattern for mixin properties static getter name.
|
||||
/// </summary>
|
||||
/// <remarks>Default value is "GetXxx". Standard string format.</remarks>
|
||||
public string StaticMixinGetterPattern { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether we should flag out-of-date models.
|
||||
/// </summary>
|
||||
/// <remarks>Models become out-of-date when data types or content types are updated. When this
|
||||
/// setting is activated the ~/App_Data/Models/ood.txt file is then created. When models are
|
||||
/// generated through the dashboard, the files is cleared. Default value is <c>false</c>.</remarks>
|
||||
public bool FlagOutOfDateModels { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the CLR name source.
|
||||
/// </summary>
|
||||
public ClrNameSource ClrNameSource { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the models directory.
|
||||
/// </summary>
|
||||
/// <remarks>Default is ~/App_Data/Models but that can be changed.</remarks>
|
||||
public string ModelsDirectory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to accept an unsafe value for ModelsDirectory.
|
||||
/// </summary>
|
||||
/// <remarks>An unsafe value is an absolute path, or a relative path pointing outside
|
||||
/// of the website root.</remarks>
|
||||
public bool AcceptUnsafeModelsDirectory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating the debug log level.
|
||||
/// </summary>
|
||||
/// <remarks>0 means minimal (safe on live site), anything else means more and more details (maybe not safe).</remarks>
|
||||
public int DebugLevel { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
namespace Umbraco.ModelsBuilder.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the models generation modes.
|
||||
/// </summary>
|
||||
public enum ModelsMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Do not generate models.
|
||||
/// </summary>
|
||||
Nothing = 0, // default value
|
||||
|
||||
/// <summary>
|
||||
/// Generate models in memory.
|
||||
/// When: a content type change occurs.
|
||||
/// </summary>
|
||||
/// <remarks>The app does not restart. Models are available in views exclusively.</remarks>
|
||||
PureLive,
|
||||
|
||||
/// <summary>
|
||||
/// Generate models in AppData.
|
||||
/// When: generation is triggered.
|
||||
/// </summary>
|
||||
/// <remarks>Generation can be triggered from the dashboard. The app does not restart.
|
||||
/// Models are not compiled and thus are not available to the project.</remarks>
|
||||
AppData,
|
||||
|
||||
/// <summary>
|
||||
/// Generate models in AppData.
|
||||
/// When: a content type change occurs, or generation is triggered.
|
||||
/// </summary>
|
||||
/// <remarks>Generation can be triggered from the dashboard. The app does not restart.
|
||||
/// Models are not compiled and thus are not available to the project.</remarks>
|
||||
LiveAppData,
|
||||
|
||||
/// <summary>
|
||||
/// Generates models in AppData and compiles them into a Dll into ~/bin (the app restarts).
|
||||
/// When: generation is triggered.
|
||||
/// </summary>
|
||||
/// <remarks>Generation can be triggered from the dashboard. The app does restart. Models
|
||||
/// are available to the entire project.</remarks>
|
||||
Dll,
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>Generation can be triggered from the dashboard. The app does restart. Models
|
||||
/// are available to the entire project.</remarks>
|
||||
LiveDll
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
namespace Umbraco.ModelsBuilder.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extensions for the <see cref="ModelsMode"/> enumeration.
|
||||
/// </summary>
|
||||
public static class ModelsModeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the mode is LiveAnything or PureLive.
|
||||
/// </summary>
|
||||
public static bool IsLive(this ModelsMode modelsMode)
|
||||
{
|
||||
return
|
||||
modelsMode == ModelsMode.PureLive
|
||||
|| modelsMode == ModelsMode.LiveDll
|
||||
|| modelsMode == ModelsMode.LiveAppData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the mode is LiveAnything but not PureLive.
|
||||
/// </summary>
|
||||
public static bool IsLiveNotPure(this ModelsMode modelsMode)
|
||||
{
|
||||
return
|
||||
modelsMode == ModelsMode.LiveDll
|
||||
|| modelsMode == ModelsMode.LiveAppData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the mode is [Live]Dll.
|
||||
/// </summary>
|
||||
public static bool IsAnyDll(this ModelsMode modelsMode)
|
||||
{
|
||||
return
|
||||
modelsMode == ModelsMode.Dll
|
||||
|| modelsMode == ModelsMode.LiveDll;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the mode supports explicit generation (as opposed to pure live).
|
||||
/// </summary>
|
||||
public static bool SupportsExplicitGeneration(this ModelsMode modelsMode)
|
||||
{
|
||||
return
|
||||
modelsMode == ModelsMode.Dll
|
||||
|| modelsMode == ModelsMode.LiveDll
|
||||
|| modelsMode == ModelsMode.AppData
|
||||
|| modelsMode == ModelsMode.LiveAppData;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using System.Threading;
|
||||
using Umbraco.Core.Configuration;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for the <see cref="UmbracoConfig"/> class.
|
||||
/// </summary>
|
||||
public static class UmbracoConfigExtensions
|
||||
{
|
||||
private static Config _config;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the models builder configuration.
|
||||
/// </summary>
|
||||
/// <param name="umbracoConfig">The umbraco configuration.</param>
|
||||
/// <returns>The models builder configuration.</returns>
|
||||
/// <remarks>Getting the models builder configuration freezes its state,
|
||||
/// and any attempt at modifying the configuration using the Setup method
|
||||
/// will be ignored.</remarks>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<br />(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("<ul>");
|
||||
|
||||
sb.Append("<li>The <strong>models factory</strong> is ");
|
||||
sb.Append(config.EnableFactory || config.ModelsMode == ModelsMode.PureLive
|
||||
? "enabled"
|
||||
: "not enabled. Umbraco will <em>not</em> use models");
|
||||
sb.Append(".</li>");
|
||||
|
||||
sb.Append("<li>The <strong>API</strong> is ");
|
||||
if (config.ApiInstalled && config.EnableApi)
|
||||
{
|
||||
sb.Append("installed and enabled");
|
||||
if (!config.IsDebug) sb.Append(".<br />However, the API runs only with <em>debug</em> 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(".<br />");
|
||||
if (!config.ApiServer)
|
||||
sb.Append("External tools such as Visual Studio <em>cannot</em> use the API");
|
||||
else
|
||||
sb.Append("<span style=\"color:orange;font-weight:bold;\">The API endpoint is open on this server</span>");
|
||||
sb.Append(".</li>");
|
||||
|
||||
sb.Append(config.ModelsMode != ModelsMode.Nothing
|
||||
? $"<li><strong>{config.ModelsMode} models</strong> are enabled.</li>"
|
||||
: "<li>No models mode is specified: models will <em>not</em> be generated.</li>");
|
||||
|
||||
sb.Append($"<li>Models namespace is {config.ModelsNamespace}.</li>");
|
||||
|
||||
sb.Append("<li>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(".</li>");
|
||||
|
||||
sb.Append("<li>Tracking of <strong>out-of-date models</strong> is ");
|
||||
sb.Append(config.FlagOutOfDateModels ? "enabled" : "not enabled");
|
||||
sb.Append(".</li>");
|
||||
|
||||
sb.Append("</ul>");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.ModelsBuilder
|
||||
{
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
public static void RemoveAll<T>(this IList<T> list, Func<T, bool> predicate)
|
||||
{
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (predicate(list[i]))
|
||||
{
|
||||
list.RemoveAt(i--); // i-- is important here!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<T> And<T>(this IEnumerable<T> enumerable, T item)
|
||||
{
|
||||
foreach (var x in enumerable) yield return x;
|
||||
yield return item;
|
||||
}
|
||||
|
||||
public static IEnumerable<T> AndIfNotNull<T>(this IEnumerable<T> enumerable, T item)
|
||||
where T : class
|
||||
{
|
||||
foreach (var x in enumerable) yield return x;
|
||||
if (item != null)
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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...
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that no model should be generated for a specified content type alias.
|
||||
/// </summary>
|
||||
/// <remarks>When a content type is ignored, its descendants are also ignored.</remarks>
|
||||
/// <remarks>Supports trailing wildcard eg "foo*".</remarks>
|
||||
[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*/)
|
||||
{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.ModelsBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that no model should be generated for a specified property type alias.
|
||||
/// </summary>
|
||||
/// <remarks>Supports trailing wildcard eg "foo*".</remarks>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
public sealed class IgnorePropertyTypeAttribute : Attribute
|
||||
{
|
||||
public IgnorePropertyTypeAttribute(string alias)
|
||||
{}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a (partial) class defines the model type for a specific alias.
|
||||
/// </summary>
|
||||
/// <remarks>Though a model will be generated - so that is the way to register a rename.</remarks>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public sealed class ImplementContentTypeAttribute : Attribute
|
||||
{
|
||||
public ImplementContentTypeAttribute(string alias)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.ModelsBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that a property implements a given property alias.
|
||||
/// </summary>
|
||||
/// <remarks>And therefore it should not be generated.</remarks>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
|
||||
public sealed class ImplementPropertyTypeAttribute : Attribute
|
||||
{
|
||||
public ImplementPropertyTypeAttribute(string alias)
|
||||
{
|
||||
Alias = alias;
|
||||
}
|
||||
|
||||
public string Alias { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.ModelsBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates the default base class for models.
|
||||
/// </summary>
|
||||
/// <remarks>Otherwise it is PublishedContentModel. Would make sense to inherit from PublishedContentModel.</remarks>
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
|
||||
public sealed class ModelsBaseClassAttribute : Attribute
|
||||
{
|
||||
public ModelsBaseClassAttribute(Type type)
|
||||
{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.ModelsBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that an Assembly is a Models Builder assembly.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Assembly /*, AllowMultiple = false, Inherited = false*/)]
|
||||
public sealed class ModelsBuilderAssemblyAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the assembly is a PureLive assembly.
|
||||
/// </summary>
|
||||
/// <remarks>A Models Builder assembly can be either PureLive or normal Dll.</remarks>
|
||||
public bool PureLive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a hash value representing the state of the custom source code files
|
||||
/// and the Umbraco content types that were used to generate and compile the assembly.
|
||||
/// </summary>
|
||||
public string SourceHash { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.ModelsBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates the models namespace.
|
||||
/// </summary>
|
||||
/// <remarks>Will override anything else that might come from settings.</remarks>
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
|
||||
public sealed class ModelsNamespaceAttribute : Attribute
|
||||
{
|
||||
public ModelsNamespaceAttribute(string modelsNamespace)
|
||||
{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates namespaces that should be used in generated models (in using clauses).
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)]
|
||||
public sealed class ModelsUsingAttribute : Attribute
|
||||
{
|
||||
public ModelsUsingAttribute(string usingNamespace)
|
||||
{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +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")]
|
||||
@@ -1,48 +0,0 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Web;
|
||||
|
||||
namespace Umbraco.ModelsBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods to models.
|
||||
/// </summary>
|
||||
public static class PublishedElementExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the value of a property.
|
||||
/// </summary>
|
||||
public static TValue Value<TModel, TValue>(this TModel model, Expression<Func<TModel, TValue>> property, string culture = ".", string segment = ".")
|
||||
where TModel : IPublishedElement
|
||||
{
|
||||
var alias = GetAlias(model, property);
|
||||
return model.Value<TValue>(alias, culture, segment);
|
||||
}
|
||||
|
||||
private static string GetAlias<TModel, TValue>(TModel model, Expression<Func<TModel, TValue>> property)
|
||||
{
|
||||
if (property.NodeType != ExpressionType.Lambda)
|
||||
throw new ArgumentException("Not a proper lambda expression (lambda).", nameof(property));
|
||||
|
||||
var lambda = (LambdaExpression) property;
|
||||
var lambdaBody = lambda.Body;
|
||||
|
||||
if (lambdaBody.NodeType != ExpressionType.MemberAccess)
|
||||
throw new ArgumentException("Not a proper lambda expression (body).", nameof(property));
|
||||
|
||||
var memberExpression = (MemberExpression) lambdaBody;
|
||||
if (memberExpression.Expression.NodeType != ExpressionType.Parameter)
|
||||
throw new ArgumentException("Not a proper lambda expression (member).", nameof(property));
|
||||
|
||||
var member = memberExpression.Member;
|
||||
|
||||
var attribute = member.GetCustomAttribute<ImplementPropertyTypeAttribute>();
|
||||
if (attribute == null)
|
||||
throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute.");
|
||||
|
||||
return attribute.Alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<int, string>[] PreValues(this PublishedPropertyType propertyType)
|
||||
//{
|
||||
// return ApplicationContext.Current.Services.DataTypeService
|
||||
// .GetPreValuesCollectionByDataTypeId(propertyType.DataType.Id)
|
||||
// .PreValuesAsArray
|
||||
// .Select(x => new KeyValuePair<int, string>(x.Id, x.Value))
|
||||
// .ToArray();
|
||||
//}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.ModelsBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that an Assembly is a PureLive models assembly.
|
||||
/// </summary>
|
||||
/// <remarks>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.</remarks>
|
||||
[Obsolete("Should use ModelsBuilderAssemblyAttribute but that requires a change in Umbraco Core.")]
|
||||
[AttributeUsage(AttributeTargets.Assembly /*, AllowMultiple = false, Inherited = false*/)]
|
||||
public sealed class PureLiveAssemblyAttribute : Attribute
|
||||
{ }
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Web.Compilation;
|
||||
using System.Web.Hosting;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Umbraco.Core;
|
||||
|
||||
namespace Umbraco.ModelsBuilder
|
||||
{
|
||||
internal static class ReferencedAssemblies
|
||||
{
|
||||
private static readonly Lazy<IEnumerable<string>> LazyLocations;
|
||||
private static readonly Lazy<IEnumerable<PortableExecutableReference>> LazyReferences;
|
||||
|
||||
static ReferencedAssemblies()
|
||||
{
|
||||
LazyLocations = new Lazy<IEnumerable<string>>(() => HostingEnvironment.IsHosted
|
||||
? GetAllReferencedAssembliesLocationFromBuildManager()
|
||||
: GetAllReferencedAssembliesFromDomain());
|
||||
|
||||
LazyReferences = new Lazy<IEnumerable<PortableExecutableReference>>(() => Locations
|
||||
.Select(x => MetadataReference.CreateFromFile(x))
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assembly locations of all the referenced assemblies, that
|
||||
/// are not dynamic, and have a non-null nor empty location.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> Locations => LazyLocations.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metadata reference of all the referenced assemblies.
|
||||
/// </summary>
|
||||
public static IEnumerable<PortableExecutableReference> References => LazyReferences.Value;
|
||||
|
||||
// hosted, get referenced assemblies from the BuildManader and filter
|
||||
private static IEnumerable<string> GetAllReferencedAssembliesLocationFromBuildManager()
|
||||
{
|
||||
return BuildManager.GetReferencedAssemblies()
|
||||
.Cast<Assembly>()
|
||||
.Where(x => !x.IsDynamic && !x.Location.IsNullOrWhiteSpace())
|
||||
.Select(x => x.Location)
|
||||
.And(typeof(ReferencedAssemblies).Assembly.Location) // always include ourselves
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// non-hosted, do our best
|
||||
private static IEnumerable<string> GetAllReferencedAssembliesFromDomain()
|
||||
{
|
||||
//TODO: This method has bugs since I've been stuck in an infinite loop with it, though this shouldn't
|
||||
// execute while in the web application anyways.
|
||||
|
||||
var assemblies = new List<Assembly>();
|
||||
var tmp1 = new List<Assembly>();
|
||||
var failed = new List<AssemblyName>();
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(x => x.IsDynamic == false)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.Location))) // though... IsDynamic should be enough?
|
||||
{
|
||||
assemblies.Add(assembly);
|
||||
tmp1.Add(assembly);
|
||||
}
|
||||
|
||||
// fixme - AssemblyUtility questions
|
||||
// - should we also load everything that's in the same directory?
|
||||
// - do we want to load in the current app domain?
|
||||
// - if this runs within Umbraco then we have already loaded them all?
|
||||
|
||||
while (tmp1.Count > 0)
|
||||
{
|
||||
var tmp2 = tmp1
|
||||
.SelectMany(x => x.GetReferencedAssemblies())
|
||||
.Distinct()
|
||||
.Where(x => assemblies.All(xx => x.FullName != xx.FullName)) // we don't have it already
|
||||
.Where(x => failed.All(xx => x.FullName != xx.FullName)) // it hasn't failed already
|
||||
.ToArray();
|
||||
tmp1.Clear();
|
||||
foreach (var assemblyName in tmp2)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assembly = AppDomain.CurrentDomain.Load(assemblyName);
|
||||
assemblies.Add(assembly);
|
||||
tmp1.Add(assembly);
|
||||
}
|
||||
catch
|
||||
{
|
||||
failed.Add(assemblyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return assemblies.Select(x => x.Location).Distinct();
|
||||
}
|
||||
|
||||
|
||||
// ----
|
||||
|
||||
private static IEnumerable<Assembly> GetDeepReferencedAssemblies(Assembly assembly)
|
||||
{
|
||||
var visiting = new Stack<Assembly>();
|
||||
var visited = new HashSet<Assembly>();
|
||||
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
return AppDomain.CurrentDomain.Load(name);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
//Console.WriteLine(name);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.ModelsBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates a model name for a specified content alias.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)]
|
||||
public sealed class RenameContentTypeAttribute : Attribute
|
||||
{
|
||||
public RenameContentTypeAttribute(string alias, string name)
|
||||
{}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.ModelsBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates a model name for a specified property alias.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
public sealed class RenamePropertyTypeAttribute : Attribute
|
||||
{
|
||||
public RenamePropertyTypeAttribute(string alias, string name)
|
||||
{}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.ModelsBuilder
|
||||
{
|
||||
internal static class TypeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a generic instance of a generic type with the proper actual type of an object.
|
||||
/// </summary>
|
||||
/// <param name="genericType">A generic type such as <c>Something{}</c></param>
|
||||
/// <param name="typeParmObj">An object whose type is used as generic type param.</param>
|
||||
/// <param name="ctorArgs">Arguments for the constructor.</param>
|
||||
/// <returns>A generic instance of the generic type with the proper type.</returns>
|
||||
/// <remarks>Usage... typeof (Something{}).CreateGenericInstance(object1, object2, object3) will return
|
||||
/// a Something{Type1} if object1.GetType() is Type1.</remarks>
|
||||
public static object CreateGenericInstance(this Type genericType, object typeParmObj, params object[] ctorArgs)
|
||||
{
|
||||
var type = genericType.MakeGenericType(typeParmObj.GetType());
|
||||
return Activator.CreateInstance(type, ctorArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{7020A059-C0D1-43A0-8EFD-23591A0C9AF6}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Umbraco.ModelsBuilder</RootNamespace>
|
||||
<AssemblyName>Umbraco.ModelsBuilder</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<DocumentationFile>bin\Release\Umbraco.ModelsBuilder.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Web" />
|
||||
<Reference Include="System.Web.ApplicationServices" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SolutionInfo.cs">
|
||||
<Link>Properties\SolutionInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Api\ApiBasicAuthFilter.cs" />
|
||||
<Compile Include="Api\ApiClient.cs" />
|
||||
<Compile Include="Api\ApiHelper.cs" />
|
||||
<Compile Include="Api\ApiVersion.cs" />
|
||||
<Compile Include="Api\GetModelsData.cs" />
|
||||
<Compile Include="Api\ModelsBuilderApiController.cs" />
|
||||
<Compile Include="Api\TokenData.cs" />
|
||||
<Compile Include="Api\ValidateClientVersionData.cs" />
|
||||
<Compile Include="Building\Builder.cs" />
|
||||
<Compile Include="Building\CodeDomBuilder.cs" />
|
||||
<Compile Include="Building\CodeParser.cs" />
|
||||
<Compile Include="Building\Compiler.cs" />
|
||||
<Compile Include="Building\CompilerException.cs" />
|
||||
<Compile Include="Building\ParseResult.cs" />
|
||||
<Compile Include="Building\PropertyModel.cs" />
|
||||
<Compile Include="Building\TextBuilder.cs" />
|
||||
<Compile Include="Building\TextHeaderWriter.cs" />
|
||||
<Compile Include="Building\TypeModel.cs" />
|
||||
<Compile Include="Configuration\ClrNameSource.cs" />
|
||||
<Compile Include="Configuration\Config.cs" />
|
||||
<Compile Include="Configuration\ModelsMode.cs" />
|
||||
<Compile Include="Configuration\ModelsModeExtensions.cs" />
|
||||
<Compile Include="Configuration\UmbracoConfigExtensions.cs" />
|
||||
<Compile Include="Dashboard\BuilderDashboardHelper.cs" />
|
||||
<Compile Include="EnumerableExtensions.cs" />
|
||||
<Compile Include="IgnoreContentTypeAttribute.cs" />
|
||||
<Compile Include="IgnorePropertyTypeAttribute.cs" />
|
||||
<Compile Include="ImplementContentTypeAttribute.cs" />
|
||||
<Compile Include="ImplementPropertyTypeAttribute.cs" />
|
||||
<Compile Include="ModelsBaseClassAttribute.cs" />
|
||||
<Compile Include="ModelsBuilderAssemblyAttribute.cs" />
|
||||
<Compile Include="ModelsNamespaceAttribute.cs" />
|
||||
<Compile Include="ModelsUsingAttribute.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="PublishedElementExtensions.cs" />
|
||||
<Compile Include="PublishedPropertyTypeExtensions.cs" />
|
||||
<Compile Include="PureLiveAssemblyAttribute.cs" />
|
||||
<Compile Include="ReferencedAssemblies.cs" />
|
||||
<Compile Include="RenameContentTypeAttribute.cs" />
|
||||
<Compile Include="RenamePropertyTypeAttribute.cs" />
|
||||
<Compile Include="TypeExtensions.cs" />
|
||||
<Compile Include="Umbraco\Application.cs" />
|
||||
<Compile Include="Umbraco\HashCombiner.cs" />
|
||||
<Compile Include="Umbraco\HashHelper.cs" />
|
||||
<Compile Include="Umbraco\LiveModelsProvider.cs" />
|
||||
<Compile Include="Umbraco\ModelsBuilderApplication.cs" />
|
||||
<Compile Include="Umbraco\ModelsBuilderBackOfficeController.cs" />
|
||||
<Compile Include="Umbraco\ModelsBuilderComponent.cs" />
|
||||
<Compile Include="Umbraco\ModelsGenerationError.cs" />
|
||||
<Compile Include="Umbraco\OutOfDateModelsStatus.cs" />
|
||||
<Compile Include="Umbraco\PublishedModelUtility.cs" />
|
||||
<Compile Include="Umbraco\PureLiveModelFactory.cs" />
|
||||
<Compile Include="Validation\ContentTypeModelValidator.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp">
|
||||
<Version>2.8.0</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj">
|
||||
<Project>{31785bc3-256c-4613-b2f5-a1b0bdded8c1}</Project>
|
||||
<Name>Umbraco.Core</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Umbraco.Web\Umbraco.Web.csproj">
|
||||
<Project>{651e1350-91b6-44b7-bd60-7207006d7003}</Project>
|
||||
<Name>Umbraco.Web</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -1,236 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
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;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Umbraco
|
||||
{
|
||||
public class Application
|
||||
{
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly IMediaTypeService _mediaTypeService;
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
private readonly IPublishedContentTypeFactory _publishedContentTypeFactory;
|
||||
|
||||
public Application(IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, IPublishedContentTypeFactory publishedContentTypeFactory)
|
||||
{
|
||||
_contentTypeService = contentTypeService;
|
||||
_mediaTypeService = mediaTypeService;
|
||||
_memberTypeService = memberTypeService;
|
||||
_publishedContentTypeFactory = publishedContentTypeFactory;
|
||||
}
|
||||
|
||||
#region Services
|
||||
|
||||
public IList<TypeModel> GetAllTypes()
|
||||
{
|
||||
var types = new List<TypeModel>();
|
||||
|
||||
types.AddRange(GetTypes(PublishedItemType.Content, _contentTypeService.GetAll().Cast<IContentTypeComposition>().ToArray()));
|
||||
types.AddRange(GetTypes(PublishedItemType.Media, _mediaTypeService.GetAll().Cast<IContentTypeComposition>().ToArray()));
|
||||
types.AddRange(GetTypes(PublishedItemType.Member, _memberTypeService.GetAll().Cast<IContentTypeComposition>().ToArray()));
|
||||
|
||||
return EnsureDistinctAliases(types);
|
||||
}
|
||||
|
||||
public IList<TypeModel> GetContentTypes()
|
||||
{
|
||||
var contentTypes = _contentTypeService.GetAll().Cast<IContentTypeComposition>().ToArray();
|
||||
return GetTypes(PublishedItemType.Content, contentTypes); // aliases have to be unique here
|
||||
}
|
||||
|
||||
public IList<TypeModel> GetMediaTypes()
|
||||
{
|
||||
var contentTypes = _mediaTypeService.GetAll().Cast<IContentTypeComposition>().ToArray();
|
||||
return GetTypes(PublishedItemType.Media, contentTypes); // aliases have to be unique here
|
||||
}
|
||||
|
||||
public IList<TypeModel> GetMemberTypes()
|
||||
{
|
||||
var memberTypes = _memberTypeService.GetAll().Cast<IContentTypeComposition>().ToArray();
|
||||
return GetTypes(PublishedItemType.Member, memberTypes); // aliases have to be unique here
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
private IList<TypeModel> GetTypes(PublishedItemType itemType, IContentTypeComposition[] contentTypes)
|
||||
{
|
||||
var typeModels = new List<TypeModel>();
|
||||
var uniqueTypes = new HashSet<string>();
|
||||
|
||||
// get the types and the properties
|
||||
foreach (var contentType in contentTypes)
|
||||
{
|
||||
var typeModel = new TypeModel
|
||||
{
|
||||
Id = contentType.Id,
|
||||
Alias = contentType.Alias,
|
||||
ClrName = GetClrName(contentType.Name, contentType.Alias),
|
||||
ParentId = contentType.ParentId,
|
||||
|
||||
Name = contentType.Name,
|
||||
Description = contentType.Description
|
||||
};
|
||||
|
||||
// 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}\".");
|
||||
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;
|
||||
}
|
||||
break;
|
||||
case PublishedItemType.Media:
|
||||
typeModel.ItemType = TypeModel.ItemTypes.Media;
|
||||
break;
|
||||
case PublishedItemType.Member:
|
||||
typeModel.ItemType = TypeModel.ItemTypes.Member;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(string.Format("Unsupported PublishedItemType \"{0}\".", itemType));
|
||||
}
|
||||
|
||||
typeModels.Add(typeModel);
|
||||
|
||||
foreach (var propertyType in contentType.PropertyTypes)
|
||||
{
|
||||
var propertyModel = new PropertyModel
|
||||
{
|
||||
Alias = propertyType.Alias,
|
||||
ClrName = GetClrName(propertyType.Name, propertyType.Alias),
|
||||
|
||||
Name = propertyType.Name,
|
||||
Description = propertyType.Description
|
||||
};
|
||||
|
||||
var publishedPropertyType = publishedContentType.GetPropertyType(propertyType.Alias);
|
||||
if (publishedPropertyType == null)
|
||||
throw new Exception($"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}.");
|
||||
|
||||
propertyModel.ModelClrType = publishedPropertyType.ModelClrType;
|
||||
|
||||
typeModel.Properties.Add(propertyModel);
|
||||
}
|
||||
}
|
||||
|
||||
// wire the base types
|
||||
foreach (var typeModel in typeModels.Where(x => x.ParentId > 0))
|
||||
{
|
||||
typeModel.BaseType = typeModels.SingleOrDefault(x => x.Id == typeModel.ParentId);
|
||||
// Umbraco 7.4 introduces content types containers, so even though ParentId > 0, the parent might
|
||||
// not be a content type - here we assume that BaseType being null while ParentId > 0 means that
|
||||
// the parent is a container (and we don't check).
|
||||
typeModel.IsParent = typeModel.BaseType != null;
|
||||
}
|
||||
|
||||
// discover mixins
|
||||
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.");
|
||||
|
||||
IEnumerable<IContentTypeComposition> compositionTypes;
|
||||
var contentTypeAsMedia = contentType as IMediaType;
|
||||
var contentTypeAsContent = contentType as IContentType;
|
||||
var contentTypeAsMember = contentType as IMemberType;
|
||||
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));
|
||||
|
||||
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 (compositionType.Id == contentType.ParentId) continue;
|
||||
|
||||
// add to mixins
|
||||
typeModel.MixinTypes.Add(compositionModel);
|
||||
|
||||
// mark as mixin - as well as parents
|
||||
compositionModel.IsMixin = true;
|
||||
while ((compositionModel = compositionModel.BaseType) != null)
|
||||
compositionModel.IsMixin = true;
|
||||
}
|
||||
}
|
||||
|
||||
return typeModels;
|
||||
}
|
||||
|
||||
internal static IList<TypeModel> EnsureDistinctAliases(IList<TypeModel> typeModels)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Umbraco
|
||||
{
|
||||
// because, of course, it's internal in Umbraco
|
||||
// see also System.Web.Util.HashCodeCombiner
|
||||
class HashCombiner
|
||||
{
|
||||
private long _combinedHash = 5381L;
|
||||
|
||||
public void Add(int i)
|
||||
{
|
||||
_combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i;
|
||||
}
|
||||
|
||||
public void Add(object o)
|
||||
{
|
||||
Add(o.GetHashCode());
|
||||
}
|
||||
|
||||
public void Add(DateTime d)
|
||||
{
|
||||
Add(d.GetHashCode());
|
||||
}
|
||||
|
||||
public void Add(string s)
|
||||
{
|
||||
if (s == null) return;
|
||||
Add((StringComparer.InvariantCulture).GetHashCode(s));
|
||||
}
|
||||
|
||||
public string GetCombinedHashCode()
|
||||
{
|
||||
return _combinedHash.ToString("x", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.ModelsBuilder.Building;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Umbraco
|
||||
{
|
||||
class HashHelper
|
||||
{
|
||||
public static string Hash(IDictionary<string, string> ourFiles, IEnumerable<TypeModel> 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
|
||||
|
||||
foreach (var typeModel in typeModels.OrderBy(x => x.Alias))
|
||||
{
|
||||
hash.Add("--- CONTENT TYPE MODEL ---");
|
||||
hash.Add(typeModel.Id);
|
||||
hash.Add(typeModel.Alias);
|
||||
hash.Add(typeModel.ClrName);
|
||||
hash.Add(typeModel.ParentId);
|
||||
hash.Add(typeModel.Name);
|
||||
hash.Add(typeModel.Description);
|
||||
hash.Add(typeModel.ItemType.ToString());
|
||||
hash.Add("MIXINS:" + string.Join(",", typeModel.MixinTypes.OrderBy(x => x.Id).Select(x => x.Id)));
|
||||
|
||||
foreach (var prop in typeModel.Properties.OrderBy(x => x.Alias))
|
||||
{
|
||||
hash.Add("--- PROPERTY ---");
|
||||
hash.Add(prop.Alias);
|
||||
hash.Add(prop.ClrName);
|
||||
hash.Add(prop.Name);
|
||||
hash.Add(prop.Description);
|
||||
hash.Add(prop.ModelClrType.ToString()); // see ModelType tests, want ToString() not FullName
|
||||
}
|
||||
}
|
||||
|
||||
return hash.GetCombinedHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +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 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()
|
||||
{
|
||||
// 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);
|
||||
|
||||
// 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<LiveModelsProvider>("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<LiveModelsProvider>("Generate models...");
|
||||
const int timeout = 2*60*1000; // 2 mins
|
||||
_mutex.WaitOne(timeout); // wait until it is safe, and acquire
|
||||
Current.Logger.Info<LiveModelsProvider>("Generate models now.");
|
||||
GenerateModels();
|
||||
ModelsGenerationError.Clear();
|
||||
Current.Logger.Info<LiveModelsProvider>("Generated.");
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
Current.Logger.Warn<LiveModelsProvider>("Timeout, models were NOT generated.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelsGenerationError.Report("Failed to build Live models.", e);
|
||||
Current.Logger.Error<LiveModelsProvider>("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(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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,175 +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.Configuration;
|
||||
using Umbraco.Web;
|
||||
using Umbraco.Web.UI.JavaScript;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Umbraco
|
||||
{
|
||||
/// <summary>
|
||||
/// Installs ModelsBuilder into the Umbraco site.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Don't bother installing at all, if not RuntimeLevel.Run.</para>
|
||||
/// </remarks>
|
||||
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
|
||||
public class ModelsBuilderApplication : UmbracoComponentBase, IUmbracoCoreComponent
|
||||
{
|
||||
private IRuntimeState _runtimeState;
|
||||
|
||||
public override void Compose(Composition composition)
|
||||
{
|
||||
var config = UmbracoConfig.For.ModelsBuilder();
|
||||
|
||||
if (config.ModelsMode == ModelsMode.PureLive)
|
||||
InstallLiveModels(composition.Container);
|
||||
else if (config.EnableFactory)
|
||||
InstallDefaultModelsFactory(composition.Container);
|
||||
|
||||
// always setup the dashboard
|
||||
InstallServerVars();
|
||||
}
|
||||
|
||||
public void Initialize(IRuntimeState runtimeState)
|
||||
{
|
||||
_runtimeState = runtimeState;
|
||||
|
||||
var config = UmbracoConfig.For.ModelsBuilder();
|
||||
|
||||
if (config.Enable)
|
||||
FileService.SavingTemplate += FileService_SavingTemplate;
|
||||
|
||||
if (config.ModelsMode.IsLiveNotPure())
|
||||
LiveModelsProvider.Install();
|
||||
|
||||
if (config.FlagOutOfDateModels)
|
||||
OutOfDateModelsStatus.Install();
|
||||
}
|
||||
|
||||
private void InstallDefaultModelsFactory(IServiceContainer container)
|
||||
{
|
||||
var types = Current.TypeLoader.GetTypes<PublishedContentModel>();
|
||||
var factory = new PublishedModelFactory(types);
|
||||
container.RegisterSingleton<IPublishedModelFactory>(_ => factory);
|
||||
}
|
||||
|
||||
private void InstallLiveModels(IServiceContainer container)
|
||||
{
|
||||
container.RegisterSingleton<IPublishedModelFactory, PureLiveModelFactory>();
|
||||
|
||||
// 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()
|
||||
{
|
||||
// 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<string, object> umbracoUrls))
|
||||
throw new Exception("Invalid umbracoUrls");
|
||||
|
||||
if (!serverVars.ContainsKey("umbracoPlugins"))
|
||||
throw new Exception("Missing umbracoPlugins.");
|
||||
if (!(serverVars["umbracoPlugins"] is Dictionary<string, object> 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<ModelsBuilderBackOfficeController>(controller => controller.BuildModels());
|
||||
umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings();
|
||||
};
|
||||
}
|
||||
|
||||
private Dictionary<string, object> GetModelsBuilderSettings()
|
||||
{
|
||||
if (_runtimeState.Level != RuntimeLevel.Run)
|
||||
return null;
|
||||
|
||||
var settings = new Dictionary<string, object>
|
||||
{
|
||||
{"enabled", UmbracoConfig.For.ModelsBuilder().Enable}
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void FileService_SavingTemplate(IFileService sender, Core.Events.SaveEventArgs<Core.Models.ITemplate> 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 && template.Content.IsNullOrWhiteSpace())
|
||||
{
|
||||
// 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 = Application.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,181 +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;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Umbraco
|
||||
{
|
||||
/// <summary>
|
||||
/// API controller for use in the Umbraco back office with Angular resources
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public class ModelsBuilderBackOfficeController : UmbracoAuthorizedJsonController
|
||||
{
|
||||
// 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(),
|
||||
};
|
||||
}
|
||||
|
||||
internal static void GenerateModels(string modelsDirectory, string bin)
|
||||
{
|
||||
if (!Directory.Exists(modelsDirectory))
|
||||
Directory.CreateDirectory(modelsDirectory);
|
||||
|
||||
foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs"))
|
||||
File.Delete(file);
|
||||
|
||||
var umbraco = ModelsBuilderComponent.Umbraco;
|
||||
var typeModels = umbraco.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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,183 +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.Configuration;
|
||||
using Umbraco.Web;
|
||||
using Umbraco.Web.PublishedCache.NuCache;
|
||||
using Umbraco.Web.UI.JavaScript;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Umbraco
|
||||
{
|
||||
// fixme
|
||||
// nucache components wants models so we need to setup models before
|
||||
// however for some reason, this creates a cyclic dependency? => need better debugging info
|
||||
// cos nucache is Core so we need to be Core too
|
||||
// also... should have a generic "modelsbuilder" and "contentcache" components for dependencies!
|
||||
|
||||
[RequiredComponent(typeof(NuCacheComponent))]
|
||||
public class ModelsBuilderComponent : UmbracoComponentBase, IUmbracoCoreComponent
|
||||
{
|
||||
public override void Compose(Composition composition)
|
||||
{
|
||||
base.Compose(composition);
|
||||
composition.Container.Register<Application>(new PerContainerLifetime());
|
||||
|
||||
var config = UmbracoConfig.For.ModelsBuilder();
|
||||
|
||||
if (config.ModelsMode == ModelsMode.PureLive)
|
||||
InstallLiveModels(composition.Container);
|
||||
else if (config.EnableFactory)
|
||||
InstallDefaultModelsFactory(composition.Container);
|
||||
|
||||
// always setup the dashboard
|
||||
InstallServerVars(composition.Container.GetInstance<IRuntimeState>().Level);
|
||||
|
||||
// need to do it here 'cos NuCache wants it during compose?
|
||||
Umbraco = composition.Container.GetInstance<Application>();
|
||||
}
|
||||
|
||||
public void Initialize(Application application)
|
||||
{
|
||||
Umbraco = application;
|
||||
|
||||
var config = UmbracoConfig.For.ModelsBuilder();
|
||||
|
||||
if (config.Enable)
|
||||
FileService.SavingTemplate += FileService_SavingTemplate;
|
||||
|
||||
if (config.ModelsMode.IsLiveNotPure())
|
||||
LiveModelsProvider.Install();
|
||||
|
||||
if (config.FlagOutOfDateModels)
|
||||
OutOfDateModelsStatus.Install();
|
||||
}
|
||||
|
||||
public static Application Umbraco { get; private set; }
|
||||
|
||||
private void InstallDefaultModelsFactory(IServiceContainer container)
|
||||
{
|
||||
container.RegisterSingleton<IPublishedModelFactory>(factory
|
||||
=> new PublishedModelFactory(factory.GetInstance<TypeLoader>().GetTypes<PublishedContentModel>()));
|
||||
}
|
||||
|
||||
private void InstallLiveModels(IServiceContainer container)
|
||||
{
|
||||
container.RegisterSingleton<IPublishedModelFactory, PureLiveModelFactory>();
|
||||
|
||||
// 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");
|
||||
var umbracoUrls = umbracoUrlsObject as Dictionary<string, object>;
|
||||
if (umbracoUrls == null)
|
||||
throw new Exception("Invalid umbracoUrls");
|
||||
|
||||
if (!serverVars.ContainsKey("umbracoPlugins"))
|
||||
throw new Exception("Missing umbracoPlugins.");
|
||||
var umbracoPlugins = serverVars["umbracoPlugins"] as Dictionary<string, object>;
|
||||
if (umbracoPlugins == null)
|
||||
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<ModelsBuilderBackOfficeController>(controller => controller.BuildModels());
|
||||
umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(level);
|
||||
};
|
||||
}
|
||||
|
||||
private Dictionary<string, object> GetModelsBuilderSettings(RuntimeLevel level)
|
||||
{
|
||||
if (level != RuntimeLevel.Run)
|
||||
return null;
|
||||
|
||||
var settings = new Dictionary<string, object>
|
||||
{
|
||||
{"enabled", UmbracoConfig.For.ModelsBuilder().Enable}
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void FileService_SavingTemplate(IFileService sender, Core.Events.SaveEventArgs<Core.Models.ITemplate> 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 = Application.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.ModelsBuilder.Configuration;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Umbraco
|
||||
{
|
||||
internal static class ModelsGenerationError
|
||||
{
|
||||
public static void Clear()
|
||||
{
|
||||
var errFile = GetErrFile();
|
||||
if (errFile == null) return;
|
||||
|
||||
// "If the file to be deleted does not exist, no exception is thrown."
|
||||
File.Delete(errFile);
|
||||
}
|
||||
|
||||
public static void Report(string message, Exception e)
|
||||
{
|
||||
var errFile = GetErrFile();
|
||||
if (errFile == null) return;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(message);
|
||||
sb.Append("\r\n");
|
||||
sb.Append(e.Message);
|
||||
sb.Append("\r\n\r\n");
|
||||
sb.Append(e.StackTrace);
|
||||
sb.Append("\r\n");
|
||||
|
||||
File.WriteAllText(errFile, sb.ToString());
|
||||
}
|
||||
|
||||
public static string GetLastError()
|
||||
{
|
||||
var errFile = GetErrFile();
|
||||
if (errFile == null) return null;
|
||||
|
||||
try
|
||||
{
|
||||
return File.ReadAllText(errFile);
|
||||
}
|
||||
catch // accepted
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetErrFile()
|
||||
{
|
||||
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory;
|
||||
if (!Directory.Exists(modelsDirectory))
|
||||
return null;
|
||||
|
||||
return Path.Combine(modelsDirectory, "models.err");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Web.Hosting;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.ModelsBuilder.Configuration;
|
||||
using Umbraco.Web.Cache;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Umbraco
|
||||
{
|
||||
public sealed class OutOfDateModelsStatus
|
||||
{
|
||||
internal static void Install()
|
||||
{
|
||||
// just be sure
|
||||
if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false)
|
||||
return;
|
||||
|
||||
ContentTypeCacheRefresher.CacheUpdated += (sender, args) => Write();
|
||||
DataTypeCacheRefresher.CacheUpdated += (sender, args) => Write();
|
||||
}
|
||||
|
||||
private static string GetFlagPath()
|
||||
{
|
||||
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory;
|
||||
if (!Directory.Exists(modelsDirectory))
|
||||
Directory.CreateDirectory(modelsDirectory);
|
||||
return Path.Combine(modelsDirectory, "ood.flag");
|
||||
}
|
||||
|
||||
private static 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()
|
||||
{
|
||||
if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) return;
|
||||
var path = GetFlagPath();
|
||||
if (path == null || !File.Exists(path)) return;
|
||||
File.Delete(path);
|
||||
}
|
||||
|
||||
public static bool IsEnabled
|
||||
{
|
||||
get { return UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels; }
|
||||
}
|
||||
|
||||
public static bool IsOutOfDate
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) return false;
|
||||
var path = GetFlagPath();
|
||||
return path != null && File.Exists(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using Umbraco.Web.Composing;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Umbraco
|
||||
{
|
||||
public static class PublishedModelUtility
|
||||
{
|
||||
// looks safer but probably useless... ppl should not call these methods directly
|
||||
// and if they do... they have to take care about not doing stupid things
|
||||
|
||||
//public static PublishedPropertyType GetModelPropertyType2<T>(Expression<Func<T, object>> selector)
|
||||
// where T : PublishedContentModel
|
||||
//{
|
||||
// var type = typeof (T);
|
||||
// var s1 = type.GetField("ModelTypeAlias", BindingFlags.Public | BindingFlags.Static);
|
||||
// var alias = (s1.IsLiteral && s1.IsInitOnly && s1.FieldType == typeof(string)) ? (string)s1.GetValue(null) : null;
|
||||
// var s2 = type.GetField("ModelItemType", BindingFlags.Public | BindingFlags.Static);
|
||||
// var itemType = (s2.IsLiteral && s2.IsInitOnly && s2.FieldType == typeof(PublishedItemType)) ? (PublishedItemType)s2.GetValue(null) : 0;
|
||||
|
||||
// var contentType = PublishedContentType.Get(itemType, alias);
|
||||
// // etc...
|
||||
//}
|
||||
|
||||
public static PublishedContentType GetModelContentType(PublishedItemType itemType, string alias)
|
||||
{
|
||||
var facade = Current.UmbracoContext.PublishedSnapshot; // fixme inject!
|
||||
switch (itemType)
|
||||
{
|
||||
case PublishedItemType.Content:
|
||||
return facade.Content.GetContentType(alias);
|
||||
case PublishedItemType.Media:
|
||||
return facade.Media.GetContentType(alias);
|
||||
case PublishedItemType.Member:
|
||||
return facade.Members.GetContentType(alias);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(itemType));
|
||||
}
|
||||
}
|
||||
|
||||
public static PublishedPropertyType GetModelPropertyType<TModel, TValue>(PublishedContentType contentType, Expression<Func<TModel, TValue>> selector)
|
||||
//where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel
|
||||
{
|
||||
// fixme therefore, missing a check on TModel here
|
||||
|
||||
var expr = selector.Body as MemberExpression;
|
||||
|
||||
if (expr == null)
|
||||
throw new ArgumentException("Not a property expression.", nameof(selector));
|
||||
|
||||
// there _is_ a risk that contentType and T do not match
|
||||
// see note above : accepted risk...
|
||||
|
||||
var attr = expr.Member
|
||||
.GetCustomAttributes(typeof (ImplementPropertyTypeAttribute), false)
|
||||
.OfType<ImplementPropertyTypeAttribute>()
|
||||
.SingleOrDefault();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(attr?.Alias))
|
||||
throw new InvalidOperationException($"Could not figure out property alias for property \"{expr.Member.Name}\".");
|
||||
|
||||
return contentType.GetPropertyType(attr.Alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,601 +0,0 @@
|
||||
using System;
|
||||
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.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 File = System.IO.File;
|
||||
|
||||
namespace Umbraco.ModelsBuilder.Umbraco
|
||||
{
|
||||
internal class PureLiveModelFactory : IPublishedModelFactory, IRegisteredObject
|
||||
{
|
||||
private Assembly _modelsAssembly;
|
||||
private Infos _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary<string, Type>() };
|
||||
private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim();
|
||||
private volatile bool _hasModels; // volatile 'cos reading outside lock
|
||||
private bool _pendingRebuild;
|
||||
private readonly ProfilingLogger _logger;
|
||||
private readonly FileSystemWatcher _watcher;
|
||||
private int _ver, _skipver;
|
||||
private readonly int _debugLevel;
|
||||
private BuildManager _theBuildManager;
|
||||
|
||||
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(ProfilingLogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_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;
|
||||
if (!Directory.Exists(modelsDirectory))
|
||||
Directory.CreateDirectory(modelsDirectory);
|
||||
|
||||
// BEWARE! if the watcher is not properly released then for some reason the
|
||||
// BuildManager will start confusing types - using a 'registered object' here
|
||||
// though we should probably plug into Umbraco's MainDom - which is internal
|
||||
HostingEnvironment.RegisterObject(this);
|
||||
_watcher = new FileSystemWatcher(modelsDirectory);
|
||||
_watcher.Changed += WatcherOnChanged;
|
||||
_watcher.EnableRaisingEvents = true;
|
||||
|
||||
// get it here, this need to be fast
|
||||
_debugLevel = UmbracoConfig.For.ModelsBuilder().DebugLevel;
|
||||
}
|
||||
|
||||
#region IPublishedModelFactory
|
||||
|
||||
public IPublishedElement CreateModel(IPublishedElement element)
|
||||
{
|
||||
// get models, rebuilding them if needed
|
||||
var infos = EnsureModels()?.ModelInfos;
|
||||
if (infos == null)
|
||||
return element;
|
||||
|
||||
// be case-insensitive
|
||||
var contentTypeAlias = element.ContentType.Alias;
|
||||
|
||||
// lookup model constructor (else null)
|
||||
infos.TryGetValue(contentTypeAlias, out ModelInfo info);
|
||||
|
||||
// create model
|
||||
return info == null ? element : info.Ctor(element);
|
||||
}
|
||||
|
||||
// this runs only once the factory is ready
|
||||
// NOT when building models
|
||||
public Type MapModelType(Type type)
|
||||
{
|
||||
var infos = EnsureModels();
|
||||
return ModelType.Map(type, infos.ModelTypeMap);
|
||||
}
|
||||
|
||||
// this runs only once the factory is ready
|
||||
// NOT when building models
|
||||
public IList CreateModelList(string alias)
|
||||
{
|
||||
var infos = EnsureModels();
|
||||
|
||||
// fail fast
|
||||
if (infos == null)
|
||||
return new List<IPublishedElement>();
|
||||
|
||||
if (!infos.ModelInfos.TryGetValue(alias, out var modelInfo))
|
||||
return new List<IPublishedElement>();
|
||||
|
||||
var ctor = modelInfo.ListCtor;
|
||||
if (ctor != null) return ctor();
|
||||
|
||||
var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType);
|
||||
ctor = modelInfo.ListCtor = ReflectionUtilities.EmitCtor<Func<IList>>(declaring: listType);
|
||||
return ctor();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Compilation
|
||||
|
||||
// deadlock note
|
||||
//
|
||||
// when RazorBuildProvider_CodeGenerationStarted runs, the thread has Monitor.Enter-ed the BuildManager
|
||||
// singleton instance, through a call to CompilationLock.GetLock in BuildManager.GetVPathBuildResultInternal,
|
||||
// and now wants to lock _locker.
|
||||
// when EnsureModels runs, the thread locks _locker and then wants BuildManager to compile, which in turns
|
||||
// requires that the BuildManager can Monitor.Enter-ed itself.
|
||||
// so:
|
||||
//
|
||||
// T1 - needs to ensure models, locks _locker
|
||||
// T2 - needs to compile a view, locks BuildManager
|
||||
// hits RazorBuildProvider_CodeGenerationStarted
|
||||
// wants to lock _locker, wait
|
||||
// T1 - needs to compile models, using BuildManager
|
||||
// wants to lock itself, wait
|
||||
// <deadlock>
|
||||
//
|
||||
// until ASP.NET kills the long-running request (thread abort)
|
||||
//
|
||||
// problem is, we *want* to suspend views compilation while the models assembly is being changed else we
|
||||
// end up with views compiled and cached with the old assembly, while models come from the new assembly,
|
||||
// which gives more YSOD. so we *have* to lock _locker in RazorBuildProvider_CodeGenerationStarted.
|
||||
//
|
||||
// one "easy" solution consists in locking the BuildManager *before* _locker in EnsureModels, thus ensuring
|
||||
// we always lock in the same order, and getting rid of deadlocks - but that requires having access to the
|
||||
// current BuildManager instance, which is BuildManager.TheBuildManager, which is an internal property.
|
||||
//
|
||||
// well, that's what we are doing in this class' TheBuildManager property, using reflection.
|
||||
|
||||
private void RazorBuildProvider_CodeGenerationStarted(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_locker.EnterReadLock();
|
||||
|
||||
// just be safe - can happen if the first view is not an Umbraco view,
|
||||
// or if something went wrong and we don't have an assembly at all
|
||||
if (_modelsAssembly == null) return;
|
||||
|
||||
if (_debugLevel > 0)
|
||||
_logger.Logger.Debug<PureLiveModelFactory>("RazorBuildProvider.CodeGenerationStarted");
|
||||
var provider = sender as RazorBuildProvider;
|
||||
if (provider == null) return;
|
||||
|
||||
// add the assembly, and add a dependency to a text file that will change on each
|
||||
// compilation as in some environments (could not figure which/why) the BuildManager
|
||||
// would not re-compile the views when the models assembly is rebuilt.
|
||||
provider.AssemblyBuilder.AddAssemblyReference(_modelsAssembly);
|
||||
provider.AddVirtualPathDependency(ProjVirt);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_locker.IsReadLockHeld)
|
||||
_locker.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
// tells the factory that it should build a new generation of models
|
||||
private void ResetModels()
|
||||
{
|
||||
_logger.Logger.Debug<PureLiveModelFactory>("Resetting models.");
|
||||
|
||||
try
|
||||
{
|
||||
_locker.EnterWriteLock();
|
||||
|
||||
_hasModels = false;
|
||||
_pendingRebuild = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_locker.IsWriteLockHeld)
|
||||
_locker.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
// gets "the" build manager
|
||||
private BuildManager TheBuildManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_theBuildManager != null) return _theBuildManager;
|
||||
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);
|
||||
return _theBuildManager;
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that the factory is running with the lastest generation of models
|
||||
internal Infos EnsureModels()
|
||||
{
|
||||
if (_debugLevel > 0)
|
||||
_logger.Logger.Debug<PureLiveModelFactory>("Ensuring models.");
|
||||
|
||||
// don't use an upgradeable lock here because only 1 thread at a time could enter it
|
||||
try
|
||||
{
|
||||
_locker.EnterReadLock();
|
||||
if (_hasModels)
|
||||
return _infos;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_locker.IsReadLockHeld)
|
||||
_locker.ExitReadLock();
|
||||
}
|
||||
|
||||
var buildManagerLocked = false;
|
||||
try
|
||||
{
|
||||
// always take the BuildManager lock *before* taking the _locker lock
|
||||
// to avoid possible deadlock situations (see notes above)
|
||||
Monitor.Enter(TheBuildManager, ref buildManagerLocked);
|
||||
|
||||
_locker.EnterUpgradeableReadLock();
|
||||
|
||||
if (_hasModels) return _infos;
|
||||
|
||||
_locker.EnterWriteLock();
|
||||
|
||||
// we don't have models,
|
||||
// either they haven't been loaded from the cache yet
|
||||
// or they have been reseted and are pending a rebuild
|
||||
|
||||
using (_logger.DebugDuration<PureLiveModelFactory>("Get models.", "Got models."))
|
||||
{
|
||||
try
|
||||
{
|
||||
var assembly = GetModelsAssembly(_pendingRebuild);
|
||||
|
||||
// the one below can be used to simulate an issue with BuildManager, ie it will register
|
||||
// the models with the factory but NOT with the BuildManager, which will not recompile views.
|
||||
// this is for U4-8043 which is an obvious issue but I cannot replicate
|
||||
//_modelsAssembly = _modelsAssembly ?? assembly;
|
||||
|
||||
// the one below is the normal one
|
||||
_modelsAssembly = assembly;
|
||||
|
||||
var types = assembly.ExportedTypes.Where(x => x.Inherits<PublishedContentModel>() || x.Inherits<PublishedElementModel>());
|
||||
_infos = RegisterModels(types);
|
||||
ModelsGenerationError.Clear();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Logger.Error<PureLiveModelFactory>("Failed to build models.", e);
|
||||
_logger.Logger.Warn<PureLiveModelFactory>("Running without models."); // be explicit
|
||||
ModelsGenerationError.Report("Failed to build PureLive models.", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_modelsAssembly = null;
|
||||
_infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary<string, Type>() };
|
||||
}
|
||||
}
|
||||
|
||||
// don't even try again
|
||||
_hasModels = true;
|
||||
}
|
||||
|
||||
return _infos;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_locker.IsWriteLockHeld)
|
||||
_locker.ExitWriteLock();
|
||||
if (_locker.IsUpgradeableReadLockHeld)
|
||||
_locker.ExitUpgradeableReadLock();
|
||||
if (buildManagerLocked)
|
||||
Monitor.Exit(TheBuildManager);
|
||||
}
|
||||
}
|
||||
|
||||
private Assembly GetModelsAssembly(bool forceRebuild)
|
||||
{
|
||||
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().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<string, string>();
|
||||
|
||||
var umbraco = ModelsBuilderComponent.Umbraco;
|
||||
var typeModels = umbraco.GetAllTypes();
|
||||
var currentHash = HashHelper.Hash(ourFiles, typeModels);
|
||||
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");
|
||||
|
||||
// caching the generated models speeds up booting
|
||||
// currentHash hashes both the types & the user's partials
|
||||
|
||||
if (!forceRebuild)
|
||||
{
|
||||
_logger.Logger.Debug<PureLiveModelFactory>("Looking for cached models.");
|
||||
if (File.Exists(modelsHashFile) && File.Exists(projFile))
|
||||
{
|
||||
var cachedHash = File.ReadAllText(modelsHashFile);
|
||||
if (currentHash != cachedHash)
|
||||
{
|
||||
_logger.Logger.Debug<PureLiveModelFactory>("Found obsolete cached models.");
|
||||
forceRebuild = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Logger.Debug<PureLiveModelFactory>("Could not find cached models.");
|
||||
forceRebuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
Assembly assembly;
|
||||
if (forceRebuild == false)
|
||||
{
|
||||
// try to load the dll directly (avoid rebuilding)
|
||||
if (File.Exists(dllPathFile))
|
||||
{
|
||||
var dllPath = File.ReadAllText(dllPathFile);
|
||||
if (File.Exists(dllPath))
|
||||
{
|
||||
assembly = Assembly.LoadFile(dllPath);
|
||||
var attr = assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
|
||||
if (attr != null && attr.PureLive && attr.SourceHash == currentHash)
|
||||
{
|
||||
// if we were to resume at that revision, then _ver would keep increasing
|
||||
// and that is probably a bad idea - so, we'll always rebuild starting at
|
||||
// ver 1, but we remember we want to skip that one - so we never end up
|
||||
// with the "same but different" version of the assembly in memory
|
||||
_skipver = assembly.GetName().Version.Revision;
|
||||
|
||||
_logger.Logger.Debug<PureLiveModelFactory>("Loading cached models (dll).");
|
||||
return assembly;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mmust 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);
|
||||
if (match.Success)
|
||||
{
|
||||
text = text.Replace(match.Value, "AssemblyVersion(\"0.0.0." + _ver + "\")");
|
||||
File.WriteAllText(projFile, text);
|
||||
}
|
||||
|
||||
// generate a marker file that will be a dependency
|
||||
// see note in RazorBuildProvider_CodeGenerationStarted
|
||||
// NO: using all.generated.cs as a dependency
|
||||
//File.WriteAllText(Path.Combine(modelsDirectory, "models.dep"), "VER:" + _ver);
|
||||
|
||||
_ver++;
|
||||
assembly = BuildManager.GetCompiledAssembly(ProjVirt);
|
||||
File.WriteAllText(dllPathFile, assembly.Location);
|
||||
|
||||
_logger.Logger.Debug<PureLiveModelFactory>("Loading cached models (source).");
|
||||
return assembly;
|
||||
}
|
||||
|
||||
// need to rebuild
|
||||
_logger.Logger.Debug<PureLiveModelFactory>("Rebuilding models.");
|
||||
|
||||
// generate code, save
|
||||
var code = GenerateModelsCode(ourFiles, 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}"")]
|
||||
[assembly:System.Reflection.AssemblyVersion(""0.0.0.{ver}"")]");
|
||||
File.WriteAllText(modelsSrcFile, code);
|
||||
|
||||
// generate proj, save
|
||||
ourFiles["models.generated.cs"] = code;
|
||||
var proj = GenerateModelsProj(ourFiles);
|
||||
File.WriteAllText(projFile, proj);
|
||||
|
||||
// compile and register
|
||||
assembly = BuildManager.GetCompiledAssembly(ProjVirt);
|
||||
File.WriteAllText(dllPathFile, assembly.Location);
|
||||
|
||||
// assuming we can write and it's not going to cause exceptions...
|
||||
File.WriteAllText(modelsHashFile, currentHash);
|
||||
|
||||
_logger.Logger.Debug<PureLiveModelFactory>("Done rebuilding.");
|
||||
return assembly;
|
||||
}
|
||||
|
||||
private static Infos RegisterModels(IEnumerable<Type> types)
|
||||
{
|
||||
var ctorArgTypes = new[] { typeof (IPublishedElement) };
|
||||
var modelInfos = new Dictionary<string, ModelInfo>(StringComparer.InvariantCultureIgnoreCase);
|
||||
var map = new Dictionary<string, Type>();
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
ConstructorInfo constructor = null;
|
||||
Type parameterType = null;
|
||||
|
||||
foreach (var ctor in type.GetConstructors())
|
||||
{
|
||||
var parms = ctor.GetParameters();
|
||||
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.");
|
||||
constructor = ctor;
|
||||
parameterType = parms[0].ParameterType;
|
||||
}
|
||||
}
|
||||
|
||||
if (constructor == null)
|
||||
throw new InvalidOperationException($"Type {type.FullName} is missing a public constructor with one argument of type, or implementing, IPropertySet.");
|
||||
|
||||
var attribute = type.GetCustomAttribute<PublishedModelAttribute>(false);
|
||||
var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias;
|
||||
|
||||
if (modelInfos.TryGetValue(typeName, out ModelInfo 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);
|
||||
var gen = meth.GetILGenerator();
|
||||
gen.Emit(OpCodes.Ldarg_0);
|
||||
gen.Emit(OpCodes.Newobj, constructor);
|
||||
gen.Emit(OpCodes.Ret);
|
||||
var func = (Func<IPublishedElement, IPublishedElement>) meth.CreateDelegate(typeof (Func<IPublishedElement, IPublishedElement>));
|
||||
|
||||
modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, Ctor = func, ModelType = type };
|
||||
map[typeName] = type;
|
||||
}
|
||||
|
||||
return new Infos { ModelInfos = modelInfos.Count > 0 ? modelInfos : null, ModelTypeMap = map };
|
||||
}
|
||||
|
||||
private static string GenerateModelsCode(IDictionary<string, string> ourFiles, IList<TypeModel> typeModels)
|
||||
{
|
||||
var modelsDirectory = UmbracoConfig.For.ModelsBuilder().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 codeBuilder = new StringBuilder();
|
||||
builder.Generate(codeBuilder, builder.GetModelsToGenerate());
|
||||
var code = codeBuilder.ToString();
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
private static readonly Regex UsingRegex = new Regex("^using(.*);", RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
private static readonly Regex AattrRegex = new Regex("^\\[assembly:(.*)\\]", RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
|
||||
private static string GenerateModelsProj(IDictionary<string, string> files)
|
||||
{
|
||||
// ideally we would generate a CSPROJ file but then we'd need a BuildProvider for csproj
|
||||
// trying to keep things simple for the time being, just write everything to one big file
|
||||
|
||||
// group all 'using' at the top of the file (else fails)
|
||||
var usings = new List<string>();
|
||||
foreach (var k in files.Keys.ToList())
|
||||
files[k] = UsingRegex.Replace(files[k], m =>
|
||||
{
|
||||
usings.Add(m.Groups[1].Value);
|
||||
return string.Empty;
|
||||
});
|
||||
|
||||
// group all '[assembly:...]' at the top of the file (else fails)
|
||||
var aattrs = new List<string>();
|
||||
foreach (var k in files.Keys.ToList())
|
||||
files[k] = AattrRegex.Replace(files[k], m =>
|
||||
{
|
||||
aattrs.Add(m.Groups[1].Value);
|
||||
return string.Empty;
|
||||
});
|
||||
|
||||
var text = new StringBuilder();
|
||||
foreach (var u in usings.Distinct())
|
||||
{
|
||||
text.Append("using ");
|
||||
text.Append(u);
|
||||
text.Append(";\r\n");
|
||||
}
|
||||
foreach (var a in aattrs)
|
||||
{
|
||||
text.Append("[assembly:");
|
||||
text.Append(a);
|
||||
text.Append("]\r\n");
|
||||
}
|
||||
text.Append("\r\n\r\n");
|
||||
foreach (var f in files)
|
||||
{
|
||||
text.Append("// FILE: ");
|
||||
text.Append(f.Key);
|
||||
text.Append("\r\n\r\n");
|
||||
text.Append(f.Value);
|
||||
text.Append("\r\n\r\n\r\n");
|
||||
}
|
||||
text.Append("// EOF\r\n");
|
||||
|
||||
return text.ToString();
|
||||
}
|
||||
|
||||
internal class Infos
|
||||
{
|
||||
public Dictionary<string, Type> ModelTypeMap { get; set; }
|
||||
public Dictionary<string, ModelInfo> ModelInfos { get; set; }
|
||||
}
|
||||
|
||||
internal class ModelInfo
|
||||
{
|
||||
public Type ParameterType { get; set; }
|
||||
public Func<IPublishedElement, IPublishedElement> Ctor { get; set; }
|
||||
public Type ModelType { get; set; }
|
||||
public Func<IList> ListCtor { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Watching
|
||||
|
||||
private void WatcherOnChanged(object sender, FileSystemEventArgs args)
|
||||
{
|
||||
var changed = args.Name;
|
||||
|
||||
// don't reset when our files change because we are building!
|
||||
//
|
||||
// comment it out, and always ignore our files, because it seems that some
|
||||
// race conditions can occur on slow Cloud filesystems and then we keep
|
||||
// rebuilding
|
||||
|
||||
//if (_building && OurFiles.Contains(changed))
|
||||
//{
|
||||
// //_logger.Logger.Info<PureLiveModelFactory>("Ignoring files self-changes.");
|
||||
// return;
|
||||
//}
|
||||
|
||||
// always ignore our own file changes
|
||||
if (OurFiles.Contains(changed))
|
||||
return;
|
||||
|
||||
_logger.Logger.Info<PureLiveModelFactory>("Detected files changes.");
|
||||
|
||||
ResetModels();
|
||||
}
|
||||
|
||||
public void Stop(bool immediate)
|
||||
{
|
||||
_watcher.EnableRaisingEvents = false;
|
||||
_watcher.Dispose();
|
||||
HostingEnvironment.UnregisterObject(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to validate the aliases for the content type when MB is enabled to ensure that
|
||||
/// no illegal aliases are used
|
||||
/// </summary>
|
||||
internal class ContentTypeModelValidator : ContentTypeModelValidatorBase<DocumentTypeSave, PropertyTypeBasic>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to validate the aliases for the content type when MB is enabled to ensure that
|
||||
/// no illegal aliases are used
|
||||
/// </summary>
|
||||
internal class MediaTypeModelValidator : ContentTypeModelValidatorBase<MediaTypeSave, PropertyTypeBasic>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to validate the aliases for the content type when MB is enabled to ensure that
|
||||
/// no illegal aliases are used
|
||||
/// </summary>
|
||||
internal class MemberTypeModelValidator : ContentTypeModelValidatorBase<MemberTypeSave, MemberPropertyTypeBasic>
|
||||
{
|
||||
}
|
||||
|
||||
internal abstract class ContentTypeModelValidatorBase<TModel, TProperty> : EditorValidator<TModel>
|
||||
where TModel: ContentTypeSave<TProperty>
|
||||
where TProperty: PropertyTypeBasic
|
||||
{
|
||||
protected override IEnumerable<ValidationResult> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,7 @@
|
||||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.4.0" />
|
||||
<PackageReference Include="System.Threading.Tasks.Parallel" Version="4.3.0" />
|
||||
<PackageReference Include="System.Threading.Thread" Version="4.3.0" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
|
||||
<PackageReference Include="System.Xml.ReaderWriter" Version="4.3.1" />
|
||||
<PackageReference Include="System.Xml.XDocument" Version="4.3.0" />
|
||||
<PackageReference Include="System.Xml.XPath" Version="4.3.0" />
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
<PackageReference Include="SharpZipLib" Version="0.86.0" />
|
||||
<PackageReference Include="Umbraco.SqlServerCE" Version="4.0.0.1" />
|
||||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.4.0" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
|
||||
<PackageReference Include="log4net" Version="2.0.8" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
angular.module("umbraco")
|
||||
.directive("umbGridEvents",
|
||||
function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
replace: false,
|
||||
link: function(scope, el, attrs) {
|
||||
scope.$el = el;
|
||||
}
|
||||
};
|
||||
});
|
||||
+3
-9
@@ -22,8 +22,6 @@ angular.module("umbraco.directives")
|
||||
//we always fetch the default one, and then override parts with our own
|
||||
tinyMceService.configuration().then(function (tinyMceConfig) {
|
||||
|
||||
|
||||
|
||||
//config value from general tinymce.config file
|
||||
var validElements = tinyMceConfig.validElements;
|
||||
var fallbackStyles = [{title: "Page header", block: "h2"}, {title: "Section header", block: "h3"}, {title: "Paragraph header", block: "h4"}, {title: "Normal", block: "p"}, {title: "Quote", block: "blockquote"}, {title: "Code", block: "code"}];
|
||||
@@ -51,12 +49,10 @@ angular.module("umbraco.directives")
|
||||
await.push(assetsService.loadJs("lib/tinymce/tinymce.min.js", scope));
|
||||
}
|
||||
|
||||
|
||||
if(scope.configuration && scope.configuration.toolbar){
|
||||
toolbar = scope.configuration.toolbar.join(' | ');
|
||||
}
|
||||
|
||||
|
||||
if(scope.configuration && scope.configuration.stylesheets){
|
||||
angular.forEach(scope.configuration.stylesheets, function(stylesheet, key){
|
||||
|
||||
@@ -91,7 +87,7 @@ angular.module("umbraco.directives")
|
||||
}));
|
||||
});
|
||||
}else{
|
||||
stylesheets.push("views/propertyeditors/grid/config/grid.default.rtestyles.css");
|
||||
stylesheets.push("common/directives/components/grid/grid.default.rtestyles.css");
|
||||
styleFormats = fallbackStyles;
|
||||
}
|
||||
|
||||
@@ -120,7 +116,6 @@ angular.module("umbraco.directives")
|
||||
cache_suffix: "?umb__rnd=" + Umbraco.Sys.ServerVariables.application.cacheBuster
|
||||
};
|
||||
|
||||
|
||||
if (tinyMceConfig.customConfig) {
|
||||
|
||||
//if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to
|
||||
@@ -163,7 +158,6 @@ angular.module("umbraco.directives")
|
||||
//set the reference
|
||||
tinyMceEditor = editor;
|
||||
|
||||
|
||||
//enable browser based spell checking
|
||||
editor.on('init', function (e) {
|
||||
|
||||
@@ -212,7 +206,7 @@ angular.module("umbraco.directives")
|
||||
.css("margin-top", "0")
|
||||
.css("width", tinyMceWidth);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
// unpin toolbar to top of screen
|
||||
@@ -224,7 +218,7 @@ angular.module("umbraco.directives")
|
||||
|
||||
// reset padding in top of mce so the content does not "jump" up
|
||||
_tinyMceEditArea.css("padding-top", "0");
|
||||
|
||||
|
||||
_toolbar.css("position", "static");
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name umbraco.resources.gridResource
|
||||
* @description Handles retrieving grid data
|
||||
**/
|
||||
function gridResource($http, umbRequestHelper) {
|
||||
return {
|
||||
|
||||
getGridContentTypes: function () {
|
||||
return umbRequestHelper.resourcePromise(
|
||||
$http.get(
|
||||
umbRequestHelper.getApiUrl(
|
||||
"gridApiBaseUrl",
|
||||
"GetContentTypes")),
|
||||
"Failed to get grid content types");
|
||||
},
|
||||
|
||||
getScaffold: function (id) {
|
||||
return umbRequestHelper.resourcePromise(
|
||||
$http.get(
|
||||
umbRequestHelper.getApiUrl(
|
||||
"gridApiBaseUrl",
|
||||
"GetScaffold", {id: id})),
|
||||
"Failed to get scaffold");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
angular.module('umbraco.resources').factory('gridResource', gridResource);
|
||||
@@ -10,6 +10,38 @@
|
||||
|
||||
return {
|
||||
|
||||
|
||||
mapGridValueToPersistableModel: function (model) {
|
||||
// fixme - we now really only need to remove the properties collections of items
|
||||
var persistableModel = {
|
||||
rows: _.map(model.sections[0].rows,
|
||||
function (row) {
|
||||
return {
|
||||
alias: row.name,
|
||||
settings: {}, // fixme - add the settings
|
||||
cells: _.map(row.cells,
|
||||
function (cell) {
|
||||
return {
|
||||
settings: {},
|
||||
items: _.map(cell.items,
|
||||
function (item) {
|
||||
return {
|
||||
type: item.type,
|
||||
values: _.object(_.map(item.properties,
|
||||
function (prop) {
|
||||
return [prop.alias, prop.value];
|
||||
}))
|
||||
};
|
||||
})
|
||||
};
|
||||
})
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
return persistableModel;
|
||||
},
|
||||
|
||||
formatChangePasswordModel: function(model) {
|
||||
if (!model) {
|
||||
return null;
|
||||
|
||||
@@ -114,13 +114,12 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.umb-card-grid .umb-card-grid-item:hover,
|
||||
.umb-card-grid .umb-card-grid-item:focus,
|
||||
.umb-card-grid .umb-card-grid-item:hover > *,
|
||||
.umb-card-grid .umb-card-grid-item:focus > * {
|
||||
.umb-card-grid .umb-card-grid-item:focus {
|
||||
background: @gray-10;
|
||||
//color: white;
|
||||
cursor: pointer;
|
||||
@@ -140,6 +139,17 @@
|
||||
color: @gray-3;
|
||||
}
|
||||
|
||||
.umb-card-grid .umb-card-grid__checkmark {
|
||||
font-size: 13px;
|
||||
color: @white;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
line-height: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.umb-card-grid .umb-card-grid-item__loading {
|
||||
position: absolute;
|
||||
background-color: rgba(255,255,255,0.8);
|
||||
|
||||
-363
@@ -1,363 +0,0 @@
|
||||
var uSkyGridConfig = [
|
||||
{
|
||||
|
||||
style:[
|
||||
{
|
||||
label: "Set a background image",
|
||||
description: "Set a row background",
|
||||
key: "background-image",
|
||||
view: "imagepicker",
|
||||
modifier: "url({0})"
|
||||
},
|
||||
|
||||
{
|
||||
label: "Set a font color",
|
||||
description: "Pick a color",
|
||||
key: "color",
|
||||
view: "colorpicker"
|
||||
}
|
||||
],
|
||||
|
||||
config:[
|
||||
{
|
||||
label: "Preview",
|
||||
description: "Display a live preview",
|
||||
key: "preview",
|
||||
view: "boolean"
|
||||
},
|
||||
|
||||
{
|
||||
label: "Class",
|
||||
description: "Set a css class",
|
||||
key: "class",
|
||||
view: "textstring"
|
||||
}
|
||||
],
|
||||
|
||||
layouts: [
|
||||
{
|
||||
grid: 12,
|
||||
percentage: 100,
|
||||
|
||||
|
||||
rows: [
|
||||
{
|
||||
name: "Single column",
|
||||
columns: [{
|
||||
grid: 12,
|
||||
percentage: 100
|
||||
}]
|
||||
},
|
||||
|
||||
{
|
||||
name: "Article",
|
||||
models: [{
|
||||
grid: 4,
|
||||
percentage: 33.3,
|
||||
allowed: ["media","quote"]
|
||||
}, {
|
||||
grid: 8,
|
||||
percentage: 66.6,
|
||||
allowed: ["rte"]
|
||||
}]
|
||||
},
|
||||
|
||||
{
|
||||
name: "Article, reverse",
|
||||
models: [
|
||||
{
|
||||
grid: 8,
|
||||
percentage: 66.6,
|
||||
allowed: ["rte","macro"]
|
||||
},
|
||||
{
|
||||
grid: 4,
|
||||
percentage: 33.3,
|
||||
allowed: ["media","quote","embed"]
|
||||
}]
|
||||
},
|
||||
{
|
||||
name: "Profile page",
|
||||
models: [
|
||||
{
|
||||
grid: 4,
|
||||
percentage: 33.3,
|
||||
allowed: ["media"]
|
||||
},
|
||||
{
|
||||
grid: 8,
|
||||
percentage: 66.6,
|
||||
allowed: ["rte"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Headline",
|
||||
models: [
|
||||
{
|
||||
grid: 12,
|
||||
percentage: 100,
|
||||
max: 1,
|
||||
allowed: ["headline"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Three columns",
|
||||
models: [{
|
||||
grid: 4,
|
||||
percentage: 33.3,
|
||||
allowed: ["rte"]
|
||||
},
|
||||
{
|
||||
grid: 4,
|
||||
percentage: 33.3,
|
||||
allowed: ["rte"]
|
||||
},
|
||||
{
|
||||
grid: 4,
|
||||
percentage: 33.3,
|
||||
allowed: ["rte"]
|
||||
}]
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
grid: 9,
|
||||
percentage: 70,
|
||||
|
||||
cellModels: [
|
||||
{
|
||||
models: [{
|
||||
grid: 12,
|
||||
percentage: 100
|
||||
}]
|
||||
}, {
|
||||
models: [{
|
||||
grid: 6,
|
||||
percentage: 50
|
||||
}, {
|
||||
grid: 6,
|
||||
percentage: 50
|
||||
}]
|
||||
}, {
|
||||
models: [{
|
||||
grid: 4,
|
||||
percentage: 33.3
|
||||
}, {
|
||||
grid: 4,
|
||||
percentage: 33.3
|
||||
}, {
|
||||
grid: 4,
|
||||
percentage: 33.3
|
||||
}]
|
||||
}, {
|
||||
models: [{
|
||||
grid: 3,
|
||||
percentage: 25
|
||||
}, {
|
||||
grid: 3,
|
||||
percentage: 25
|
||||
}, {
|
||||
grid: 3,
|
||||
percentage: 25
|
||||
}, {
|
||||
grid: 3,
|
||||
percentage: 25
|
||||
}, ]
|
||||
}, {
|
||||
models: [{
|
||||
grid: 2,
|
||||
percentage: 16.6
|
||||
}, {
|
||||
grid: 2,
|
||||
percentage: 16.6
|
||||
}, {
|
||||
grid: 2,
|
||||
percentage: 16.6
|
||||
}, {
|
||||
grid: 2,
|
||||
percentage: 16.6
|
||||
}, {
|
||||
grid: 2,
|
||||
percentage: 16.6
|
||||
}, {
|
||||
grid: 2,
|
||||
percentage: 16.6
|
||||
}]
|
||||
}, {
|
||||
models: [{
|
||||
grid: 8,
|
||||
percentage: 60
|
||||
}, {
|
||||
grid: 4,
|
||||
percentage: 40
|
||||
}]
|
||||
}, {
|
||||
models: [{
|
||||
grid: 4,
|
||||
percentage: 40
|
||||
}, {
|
||||
grid: 8,
|
||||
percentage: 60
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
grid: 3,
|
||||
percentage: 30,
|
||||
cellModels: [
|
||||
{
|
||||
models: [{
|
||||
grid: 12,
|
||||
percentage: 100
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
grid: 3,
|
||||
percentage: 30,
|
||||
cellModels: [
|
||||
{
|
||||
models: [{
|
||||
grid: 12,
|
||||
percentage: 100
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
grid: 9,
|
||||
percentage: 70,
|
||||
cellModels: [
|
||||
{
|
||||
models: [{
|
||||
grid: 12,
|
||||
percentage: 100
|
||||
}]
|
||||
}, {
|
||||
models: [{
|
||||
grid: 6,
|
||||
percentage: 50
|
||||
}, {
|
||||
grid: 6,
|
||||
percentage: 50
|
||||
}]
|
||||
}, {
|
||||
models: [{
|
||||
grid: 4,
|
||||
percentage: 33.3
|
||||
}, {
|
||||
grid: 4,
|
||||
percentage: 33.3
|
||||
}, {
|
||||
grid: 4,
|
||||
percentage: 33.3
|
||||
}]
|
||||
}, {
|
||||
models: [{
|
||||
grid: 3,
|
||||
percentage: 25
|
||||
}, {
|
||||
grid: 3,
|
||||
percentage: 25
|
||||
}, {
|
||||
grid: 3,
|
||||
percentage: 25
|
||||
}, {
|
||||
grid: 3,
|
||||
percentage: 25
|
||||
}, ]
|
||||
}, {
|
||||
models: [{
|
||||
grid: 2,
|
||||
percentage: 16.6
|
||||
}, {
|
||||
grid: 2,
|
||||
percentage: 16.6
|
||||
}, {
|
||||
grid: 2,
|
||||
percentage: 16.6
|
||||
}, {
|
||||
grid: 2,
|
||||
percentage: 16.6
|
||||
}, {
|
||||
grid: 2,
|
||||
percentage: 16.6
|
||||
}, {
|
||||
grid: 2,
|
||||
percentage: 16.6
|
||||
}]
|
||||
}, {
|
||||
models: [{
|
||||
grid: 8,
|
||||
percentage: 60
|
||||
}, {
|
||||
grid: 4,
|
||||
percentage: 40
|
||||
}]
|
||||
}, {
|
||||
models: [{
|
||||
grid: 4,
|
||||
percentage: 40
|
||||
}, {
|
||||
grid: 8,
|
||||
percentage: 60
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
grid: 4,
|
||||
percentage: 33.3,
|
||||
cellModels: [
|
||||
{
|
||||
models: [{
|
||||
grid: 12,
|
||||
percentage: 100
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
grid: 4,
|
||||
percentage: 33.3,
|
||||
cellModels: [
|
||||
{
|
||||
models: [{
|
||||
grid: 12,
|
||||
percentage: 100
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
grid: 4,
|
||||
percentage: 33.3,
|
||||
cellModels: [
|
||||
{
|
||||
models: [{
|
||||
grid: 12,
|
||||
percentage: 100
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="umb-editor-placeholder">
|
||||
<div ng-if="!preview">
|
||||
<i class="icon {{model.icon}}"></i>
|
||||
<div class="help-text">{{model.title}}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
<div ng-if="model.config">
|
||||
<umb-property
|
||||
property="configValue"
|
||||
ng-repeat="configValue in model.config">
|
||||
<umb-property-editor model="configValue" is-pre-value="true"></umb-property-editor>
|
||||
</umb-property>
|
||||
</div>
|
||||
|
||||
<div ng-if="model.styles">
|
||||
<h4>Style</h4>
|
||||
<umb-property
|
||||
property="styleValue"
|
||||
ng-repeat="styleValue in model.styles">
|
||||
<umb-property-editor model="styleValue" is-pre-value="true"></umb-property-editor>
|
||||
</umb-property>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<form name="gridConfigEditor">
|
||||
|
||||
<h4>{{model.name}}</h4>
|
||||
<p>Settings will only save if the entered json configuration is valid</p>
|
||||
<textarea name="configSource"
|
||||
validate-on="'blur'"
|
||||
rows="20" class="umb-property-editor umb-textarea"
|
||||
umb-raw-model="model.config"></textarea>
|
||||
|
||||
<div class="alert alert-error" ng-show="gridConfigEditor.$invalid === true">
|
||||
This configuration is not valid json, and will not be saved.
|
||||
</div>
|
||||
|
||||
</form>
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
function GridEditorPicker($scope, editorService) {
|
||||
|
||||
var vm = this;
|
||||
|
||||
vm.gridEditors = $scope.model.gridEditors;
|
||||
|
||||
vm.submit = submit;
|
||||
vm.close = close;
|
||||
vm.selectGridEditor = selectGridEditor;
|
||||
|
||||
function onInit() {
|
||||
|
||||
// set default title
|
||||
if($scope.model && !$scope.model.title) {
|
||||
$scope.model.title = "Select Grid Editors";
|
||||
}
|
||||
|
||||
// make sure we have an arrow to push to
|
||||
if($scope.model && !$scope.model.selection) {
|
||||
$scope.model.selection = [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function selectGridEditor(selectedGridEditor) {
|
||||
|
||||
if(!selectedGridEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
var index = $scope.model.selection.indexOf(selectedGridEditor);
|
||||
|
||||
// select grid editor
|
||||
if(index === -1) {
|
||||
selectedGridEditor.selected = true;
|
||||
$scope.model.selection.push(selectedGridEditor);
|
||||
|
||||
// deselect grid editor
|
||||
} else {
|
||||
selectedGridEditor.selected = false;
|
||||
$scope.model.selection.splice(index, 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function submit(model) {
|
||||
if($scope.model.submit) {
|
||||
$scope.model.submit(model);
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
if($scope.model.close) {
|
||||
$scope.model.close();
|
||||
}
|
||||
}
|
||||
|
||||
onInit();
|
||||
|
||||
}
|
||||
|
||||
angular.module("umbraco").controller("Umbraco.PropertyEditors.GridPrevalueEditor.GridEditorPickerController", GridEditorPicker);
|
||||
|
||||
})();
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<div ng-controller="Umbraco.PropertyEditors.GridPrevalueEditor.GridEditorPickerController as vm">
|
||||
|
||||
<umb-editor-view>
|
||||
|
||||
<umb-editor-header
|
||||
name="model.title"
|
||||
name-locked="true"
|
||||
hide-alias="true"
|
||||
hide-icon="true"
|
||||
hide-description="true">
|
||||
</umb-editor-header>
|
||||
|
||||
<umb-editor-container>
|
||||
<umb-box>
|
||||
<umb-box-content>
|
||||
<ul class="umb-card-grid">
|
||||
<li ng-repeat="gridEditor in vm.gridEditors | orderBy:'name'"
|
||||
data-element="grid-editor-{{gridEditor.name}}"
|
||||
ng-click="vm.selectGridEditor(gridEditor)"
|
||||
class="-four-in-row">
|
||||
<a class="umb-card-grid-item" href="" title="{{ gridEditor.name }}">
|
||||
<umb-checkmark
|
||||
class="umb-card-grid__checkmark"
|
||||
ng-if="gridEditor.selected"
|
||||
checked="gridEditor.selected"
|
||||
size="xs">
|
||||
</umb-checkmark>
|
||||
<i class="{{ gridEditor.icon }}" ng-class="{'icon-item-arrangement': gridEditor.icon == null}"></i>
|
||||
{{ gridEditor.name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</umb-box-content>
|
||||
</umb-box>
|
||||
</umb-editor-container>
|
||||
|
||||
<umb-editor-footer>
|
||||
|
||||
<umb-editor-footer-content-right>
|
||||
|
||||
<umb-button
|
||||
type="button"
|
||||
button-style="link"
|
||||
label-key="general_close"
|
||||
action="vm.close()">
|
||||
</umb-button>
|
||||
|
||||
<umb-button
|
||||
type="button"
|
||||
button-style="success"
|
||||
label-key="general_submit"
|
||||
action="vm.submit(model)">
|
||||
</umb-button>
|
||||
|
||||
</umb-editor-footer-content-right>
|
||||
|
||||
</umb-editor-footer>
|
||||
|
||||
</umb-editor-view>
|
||||
|
||||
</div>
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
function RowConfigController($scope, gridResource, editorService) {
|
||||
|
||||
var vm = this;
|
||||
|
||||
$scope.currentRow = $scope.model.currentRow;
|
||||
$scope.contentTypes = $scope.model.contentTypes;
|
||||
$scope.columns = $scope.model.columns;
|
||||
|
||||
var availableEditors = [];
|
||||
|
||||
function onInit() {
|
||||
gridResource.getGridContentTypes().then(function(gridEditors){
|
||||
availableEditors = gridEditors;
|
||||
});
|
||||
}
|
||||
|
||||
vm.selectedGridEditors = [];
|
||||
vm.selectGridEditor = selectGridEditor;
|
||||
vm.removeGridEditor = removeGridEditor;
|
||||
vm.submit = submit;
|
||||
vm.close = close;
|
||||
|
||||
$scope.scaleUp = function(section, max, overflow) {
|
||||
var add = 1;
|
||||
if (overflow !== true) {
|
||||
add = (max > 1) ? 1 : max;
|
||||
}
|
||||
//var add = (max > 1) ? 1 : max;
|
||||
section.grid = section.grid + add;
|
||||
};
|
||||
|
||||
$scope.scaleDown = function(section) {
|
||||
var remove = (section.grid > 1) ? 1 : 0;
|
||||
section.grid = section.grid - remove;
|
||||
};
|
||||
|
||||
$scope.percentage = function(spans) {
|
||||
return ((spans / $scope.columns) * 100).toFixed(8);
|
||||
};
|
||||
|
||||
$scope.toggleCollection = function(collection, toggle) {
|
||||
if (toggle) {
|
||||
collection = [];
|
||||
}
|
||||
else {
|
||||
delete collection;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/****************
|
||||
area
|
||||
*****************/
|
||||
$scope.configureCell = function(cell, row) {
|
||||
if ($scope.currentCell && $scope.currentCell === cell) {
|
||||
delete $scope.currentCell;
|
||||
}
|
||||
else {
|
||||
if (cell === undefined) {
|
||||
var available = $scope.availableRowSpace;
|
||||
var space = 4;
|
||||
|
||||
if (available < 4 && available > 0) {
|
||||
space = available;
|
||||
}
|
||||
|
||||
cell = {
|
||||
grid: space
|
||||
};
|
||||
|
||||
row.areas.push(cell);
|
||||
}
|
||||
$scope.currentCell = cell;
|
||||
|
||||
updateSelectedGridEditors();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.deleteArea = function (cell, row) {
|
||||
if ($scope.currentCell === cell) {
|
||||
$scope.currentCell = undefined;
|
||||
}
|
||||
var index = row.areas.indexOf(cell)
|
||||
row.areas.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.closeArea = function() {
|
||||
$scope.currentCell = undefined;
|
||||
};
|
||||
|
||||
$scope.nameChanged = false;
|
||||
|
||||
var originalName = $scope.currentRow.name;
|
||||
|
||||
$scope.$watch("currentRow", function(row) {
|
||||
if (row) {
|
||||
|
||||
var total = 0;
|
||||
_.forEach(row.areas, function(area) {
|
||||
total = (total + area.grid);
|
||||
});
|
||||
|
||||
$scope.availableRowSpace = $scope.columns - total;
|
||||
|
||||
if (originalName) {
|
||||
if (originalName != row.name) {
|
||||
$scope.nameChanged = true;
|
||||
}
|
||||
else {
|
||||
$scope.nameChanged = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
function updateSelectedGridEditors() {
|
||||
vm.selectedGridEditors = _.filter(availableEditors, function (editor) {
|
||||
if (_.indexOf($scope.currentCell.allowed, editor.udi) >= 0) {
|
||||
editor.selected = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function selectGridEditor() {
|
||||
var gridEditorPicker = {
|
||||
view: "views/propertyEditors/grid2/dialogs/grideditorpicker.html",
|
||||
size: "small",
|
||||
gridEditors: availableEditors,
|
||||
selection: vm.selectedGridEditors,
|
||||
submit: function(model) {
|
||||
|
||||
if(model && model.selection.length > 0) {
|
||||
|
||||
if(!$scope.currentCell.allowed) {
|
||||
$scope.currentCell.allowed = [];
|
||||
}
|
||||
|
||||
model.selection.forEach(function(gridEditor){
|
||||
if($scope.currentCell.allowed.indexOf(gridEditor.udi) === -1) {
|
||||
$scope.currentCell.allowed.push(gridEditor.udi);
|
||||
}
|
||||
});
|
||||
|
||||
updateSelectedGridEditors();
|
||||
}
|
||||
|
||||
editorService.close();
|
||||
|
||||
},
|
||||
close: function() {
|
||||
editorService.close();
|
||||
}
|
||||
};
|
||||
|
||||
editorService.open(gridEditorPicker);
|
||||
}
|
||||
|
||||
function removeGridEditor(index, allowedGridEditors) {
|
||||
allowedGridEditors.splice(index, 1);
|
||||
updateSelectedGridEditors();
|
||||
}
|
||||
|
||||
function submit(model) {
|
||||
if($scope.model.submit) {
|
||||
$scope.model.submit();
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
if($scope.model.close) {
|
||||
$scope.model.close();
|
||||
}
|
||||
}
|
||||
|
||||
onInit();
|
||||
|
||||
}
|
||||
|
||||
angular.module("umbraco").controller("Umbraco.PropertyEditors.Grid2PrevalueEditor.RowConfigController", RowConfigController);
|
||||
@@ -0,0 +1,130 @@
|
||||
<div class="usky-grid usky-grid-configuration" ng-controller="Umbraco.PropertyEditors.Grid2PrevalueEditor.RowConfigController as vm">
|
||||
|
||||
<umb-editor-view>
|
||||
|
||||
<umb-editor-header
|
||||
name="currentRow.label"
|
||||
alias="currentRow.name"
|
||||
hide-icon="true"
|
||||
hide-description="true">
|
||||
</umb-editor-header>
|
||||
|
||||
<umb-editor-container>
|
||||
|
||||
<umb-box>
|
||||
|
||||
<umb-box-content>
|
||||
|
||||
<div class="umb-form-settings block-form">
|
||||
|
||||
<h5><localize key="grid_addRowConfiguration" /></h5>
|
||||
<p><localize key="grid_addRowConfigurationDetail" /></p>
|
||||
|
||||
<div class="alert alert-warn ng-scope" ng-show="nameChanged">
|
||||
<p>Modifying a row configuration alias will result in loss of
|
||||
data for any existing content that is based on this configuration.</p>
|
||||
</div>
|
||||
|
||||
<div class="uSky-templates-template"
|
||||
style="margin: 0; width: 350px; position: relative;">
|
||||
|
||||
<div class="tb" style="height: auto; border: none !important; background: none">
|
||||
<div class="tr">
|
||||
|
||||
<a class="td uSky-templates-column"
|
||||
ng-class="{last:$last, selected:cell==currentCell}"
|
||||
ng-repeat="cell in currentRow.areas"
|
||||
ng-click="configureCell(cell, currentRow)"
|
||||
ng-style="{width: percentage(cell.grid) + '%'}">
|
||||
</a>
|
||||
|
||||
<a class="td uSky-templates-column add"
|
||||
ng-click="configureCell(undefined, currentRow)"
|
||||
ng-style="{width: percentage(availableRowSpace) + '%'}">
|
||||
<i class="icon icon-add"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="currentCell" style="padding-bottom: 50px;">
|
||||
|
||||
<umb-control-group label="@general_width">
|
||||
<div class="grid-size-scaler">
|
||||
<a href ng-click="scaleDown(currentCell)">
|
||||
<i class="icon icon-remove"></i>
|
||||
</a>
|
||||
{{currentCell.grid}}
|
||||
<a href ng-click="scaleUp(currentCell, availableRowSpace, true)">
|
||||
<i class="icon icon-add"></i>
|
||||
</a>
|
||||
</div>
|
||||
</umb-control-group>
|
||||
|
||||
<umb-control-group label="@grid_maxItems" description="@grid_maxItemsDescription">
|
||||
<input type="number" ng-model="currentCell.maxItems" class="umb-property-editor-tiny" placeholder="Max" min="0" />
|
||||
</umb-control-group>
|
||||
|
||||
<umb-control-group hide-label="true">
|
||||
<i class="icon-delete red"></i>
|
||||
<a class="btn btn-small btn-link" href="" ng-click="deleteArea(currentCell, currentRow)">
|
||||
<localize key="general_delete" class="ng-isolate-scope ng-scope">Delete</localize>
|
||||
</a>
|
||||
</umb-control-group>
|
||||
|
||||
<umb-control-group label="Allowed Grid Editors" description="Select the grid editors which should be allowed for this cell">
|
||||
|
||||
<umb-node-preview
|
||||
style="max-width: 100%"
|
||||
ng-repeat="gridEditor in vm.selectedGridEditors"
|
||||
name="gridEditor.name"
|
||||
description="gridEditor.alias"
|
||||
icon="gridEditor.icon"
|
||||
allow-remove="true"
|
||||
on-remove="vm.removeGridEditor($index, currentCell.allowed)">
|
||||
</umb-node-preview>
|
||||
|
||||
<a class="umb-node-preview-add"
|
||||
style="max-width: 100%"
|
||||
href=""
|
||||
ng-click="vm.selectGridEditor()"
|
||||
prevent-default>
|
||||
<localize key="general_add">Add</localize>
|
||||
</a>
|
||||
|
||||
</umb-control-group>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</umb-box-content>
|
||||
|
||||
</umb-box>
|
||||
|
||||
</umb-editor-container>
|
||||
|
||||
<umb-editor-footer>
|
||||
|
||||
<umb-editor-footer-content-right>
|
||||
|
||||
<umb-button
|
||||
type="button"
|
||||
button-style="link"
|
||||
label-key="general_close"
|
||||
action="vm.close()">
|
||||
</umb-button>
|
||||
|
||||
<umb-button
|
||||
type="button"
|
||||
button-style="success"
|
||||
label-key="general_submit"
|
||||
action="vm.submit(model)">
|
||||
</umb-button>
|
||||
|
||||
</umb-editor-footer-content-right>
|
||||
|
||||
</umb-editor-footer>
|
||||
|
||||
</umb-editor-view>
|
||||
|
||||
</div>
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<div>
|
||||
|
||||
<h3 class="alert alert-warn ng-scope">Warning!</h3>
|
||||
|
||||
<p>
|
||||
You are deleting the row configuration '<strong>{{model.dialogData.rowName}}</strong>'
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Modifying a row configuration name will result in loss of
|
||||
data for any existing content that is based on this configuration.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<localize key="general_areyousure">Are you sure?</localize>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
angular.module("umbraco")
|
||||
.controller("Umbraco.PropertyEditors.Grid2PrevalueEditorController",
|
||||
function ($scope, $http, assetsService, $rootScope, dialogService, mediaResource, gridService, imageHelper, $timeout, editorService, gridResource) {
|
||||
|
||||
var vm = this;
|
||||
|
||||
var emptyModel = {
|
||||
styles:[
|
||||
{
|
||||
label: "Set a background image",
|
||||
description: "Set a row background",
|
||||
key: "background-image",
|
||||
view: "imagepicker",
|
||||
modifier: "url({0})"
|
||||
}
|
||||
],
|
||||
|
||||
config:[
|
||||
{
|
||||
label: "Class",
|
||||
description: "Set a css class",
|
||||
key: "class",
|
||||
view: "textstring"
|
||||
}
|
||||
],
|
||||
|
||||
columns: 12,
|
||||
templates:[
|
||||
{
|
||||
name: "1 column layout",
|
||||
sections: [
|
||||
{
|
||||
grid: 12,
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
layouts:[
|
||||
{
|
||||
label: "Headline",
|
||||
name: "Headline",
|
||||
areas: [
|
||||
{
|
||||
grid: 12
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Article",
|
||||
name: "Article",
|
||||
areas: [
|
||||
{
|
||||
grid: 4
|
||||
},
|
||||
{
|
||||
grid: 8
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
vm.configureRow = configureRow;
|
||||
|
||||
/****************
|
||||
Row
|
||||
*****************/
|
||||
|
||||
function configureRow(row) {
|
||||
|
||||
var rowCopy = angular.copy($scope.model.value.layouts);
|
||||
|
||||
if(row === undefined){
|
||||
row = {
|
||||
name: "",
|
||||
areas: []
|
||||
};
|
||||
$scope.model.value.layouts.push(row);
|
||||
}
|
||||
|
||||
var rowConfigurationEditor = {
|
||||
view: "views/propertyEditors/grid2/dialogs/rowconfig.html",
|
||||
size: "small",
|
||||
currentRow: row,
|
||||
columns: $scope.model.value.columns,
|
||||
submit: function(model) {
|
||||
editorService.close();
|
||||
},
|
||||
close: function() {
|
||||
$scope.model.value.layouts = rowCopy;
|
||||
editorService.close();
|
||||
}
|
||||
}
|
||||
|
||||
editorService.open(rowConfigurationEditor);
|
||||
|
||||
}
|
||||
|
||||
//var rowDeletesPending = false;
|
||||
$scope.deleteLayout = function(index) {
|
||||
|
||||
$scope.rowDeleteOverlay = {};
|
||||
$scope.rowDeleteOverlay.view = "views/propertyEditors/grid2/dialogs/rowdeleteconfirm.html";
|
||||
$scope.rowDeleteOverlay.dialogData = {
|
||||
rowName: $scope.model.value.layouts[index].name
|
||||
};
|
||||
$scope.rowDeleteOverlay.show = true;
|
||||
|
||||
$scope.rowDeleteOverlay.submit = function(model) {
|
||||
|
||||
$scope.model.value.layouts.splice(index, 1);
|
||||
|
||||
$scope.rowDeleteOverlay.show = false;
|
||||
$scope.rowDeleteOverlay = null;
|
||||
};
|
||||
|
||||
$scope.rowDeleteOverlay.close = function(oldModel) {
|
||||
$scope.rowDeleteOverlay.show = false;
|
||||
$scope.rowDeleteOverlay = null;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
/****************
|
||||
utillities
|
||||
*****************/
|
||||
$scope.toggleCollection = function(collection, toggle){
|
||||
if(toggle){
|
||||
collection = [];
|
||||
}else{
|
||||
delete collection;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.percentage = function(spans){
|
||||
return ((spans / $scope.model.value.columns) * 100).toFixed(8);
|
||||
};
|
||||
|
||||
$scope.zeroWidthFilter = function (cell) {
|
||||
return cell.grid > 0;
|
||||
};
|
||||
|
||||
/****************
|
||||
Config
|
||||
*****************/
|
||||
|
||||
$scope.removeConfigValue = function(collection, index){
|
||||
collection.splice(index, 1);
|
||||
};
|
||||
|
||||
var editConfigCollection = function(configValues, title, callback) {
|
||||
|
||||
$scope.editConfigCollectionOverlay = {};
|
||||
$scope.editConfigCollectionOverlay.view = "views/propertyeditors/grid2/dialogs/editconfig.html";
|
||||
$scope.editConfigCollectionOverlay.config = configValues;
|
||||
$scope.editConfigCollectionOverlay.title = title;
|
||||
$scope.editConfigCollectionOverlay.show = true;
|
||||
|
||||
$scope.editConfigCollectionOverlay.submit = function(model) {
|
||||
|
||||
callback(model.config)
|
||||
|
||||
$scope.editConfigCollectionOverlay.show = false;
|
||||
$scope.editConfigCollectionOverlay = null;
|
||||
};
|
||||
|
||||
$scope.editConfigCollectionOverlay.close = function(oldModel) {
|
||||
$scope.editConfigCollectionOverlay.show = false;
|
||||
$scope.editConfigCollectionOverlay = null;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
$scope.editConfig = function() {
|
||||
editConfigCollection($scope.model.value.config, "Settings", function(data) {
|
||||
$scope.model.value.config = data;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.editStyles = function() {
|
||||
editConfigCollection($scope.model.value.styles, "Styling", function(data){
|
||||
$scope.model.value.styles = data;
|
||||
});
|
||||
};
|
||||
|
||||
/* init grid data */
|
||||
if (!$scope.model.value || $scope.model.value === "" || !$scope.model.value.templates) {
|
||||
$scope.model.value = emptyModel;
|
||||
} else {
|
||||
|
||||
if (!$scope.model.value.columns) {
|
||||
$scope.model.value.columns = emptyModel.columns;
|
||||
}
|
||||
|
||||
|
||||
if (!$scope.model.value.config) {
|
||||
$scope.model.value.config = [];
|
||||
}
|
||||
|
||||
if (!$scope.model.value.styles) {
|
||||
$scope.model.value.styles = [];
|
||||
}
|
||||
}
|
||||
|
||||
/****************
|
||||
Clean up
|
||||
*****************/
|
||||
var unsubscribe = $scope.$on("formSubmitting", function (ev, args) {
|
||||
var ts = $scope.model.value.templates;
|
||||
var ls = $scope.model.value.layouts;
|
||||
|
||||
_.each(ts, function(t){
|
||||
_.each(t.sections, function(section, index){
|
||||
if(section.grid === 0){
|
||||
t.sections.splice(index, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
_.each(ls, function(l){
|
||||
_.each(l.areas, function(area, index){
|
||||
if(area.grid === 0){
|
||||
l.areas.splice(index, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//when the scope is destroyed we need to unsubscribe
|
||||
$scope.$on('$destroy', function () {
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
<div ng-controller="Umbraco.PropertyEditors.Grid2PrevalueEditorController as vm" class="usky-grid usky-grid-configuration">
|
||||
|
||||
<div style="max-width: 600px">
|
||||
|
||||
<div class="control-group uSky-templates">
|
||||
|
||||
<umb-control-group
|
||||
label="@grid_columns"
|
||||
description="@grid_columnsDetails">
|
||||
<input type="number" min="1" ng-model="model.value.columns" />
|
||||
</umb-control-group>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="max-width: 600px">
|
||||
|
||||
<div class="control-group uSky-templates">
|
||||
|
||||
<umb-control-group
|
||||
label="@grid_rowConfigurations"
|
||||
description="@grid_rowConfigurationsDetail">
|
||||
|
||||
<div class="control-group uSky-templates-rows">
|
||||
<ul class="unstyled"
|
||||
ui-sortable
|
||||
ng-model="model.value.layouts">
|
||||
|
||||
<li ng-repeat="row in model.value.layouts" class="clearfix">
|
||||
|
||||
<div ng-click="vm.configureRow(row)"
|
||||
class="preview-rows columns" style="margin-top: 5px; margin-bottom: 25px; float:left">
|
||||
|
||||
<div class="preview-row">
|
||||
<div class="preview-col"
|
||||
ng-class="{last:$last}"
|
||||
ng-repeat="area in row.areas | filter:zeroWidthFilter"
|
||||
ng-style="{width: percentage(area.grid) + '%', 'max-width': '100%'}">
|
||||
|
||||
<div class="preview-cell">
|
||||
<p style="font-size: 6px; line-height: 8px; text-align: center" ng-show="area.maxItems > 0">{{area.maxItems}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{row.label || row.name}}<br />
|
||||
<i class="icon-delete red"></i>
|
||||
<a class="btn btn-small btn-link" href ng-click="deleteLayout($index)">
|
||||
<localize key="general_delete" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button class="btn btn-small" prevent-default ng-click="vm.configureRow()">
|
||||
<i class="icon-add"></i> <localize key="grid_addRowConfiguration" />
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</umb-control-group>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="max-width: 600px">
|
||||
|
||||
<umb-control-group
|
||||
label="@grid_settings"
|
||||
description="@grid_settingsDetails">
|
||||
|
||||
<ul class="unstyled list-icons umb-contentpicker"
|
||||
ui-sortable
|
||||
ng-model="model.value.config">
|
||||
|
||||
<li ng-repeat="configValue in model.value.config">
|
||||
<i class="icon icon-navigation handle"></i>
|
||||
|
||||
<a href="#" prevent-default ng-click="removeConfigValue(model.value.config, $index)">
|
||||
<i class="icon icon-delete red hover-show"></i>
|
||||
<i class="icon-settings-alt-2 hover-hide"></i>
|
||||
{{configValue.label}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="unstyled list-icons">
|
||||
<li>
|
||||
<i class="icon icon-add turquoise"></i>
|
||||
|
||||
<a href="#" ng-click="editConfig()" prevent-default>
|
||||
<localize key="general_edit" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</umb-control-group>
|
||||
|
||||
<umb-control-group
|
||||
label="@grid_styles"
|
||||
description="@grid_stylesDetails">
|
||||
|
||||
<ul class="unstyled list-icons umb-contentpicker"
|
||||
ui-sortable
|
||||
ng-model="model.value.styles">
|
||||
|
||||
<li ng-repeat="style in model.value.styles">
|
||||
<i class="icon icon-navigation handle"></i>
|
||||
|
||||
<a href="#" prevent-default ng-click="removeConfigValue(model.value.styles, $index)">
|
||||
<i class="icon icon-delete red hover-show"></i>
|
||||
<i class="icon-brush hover-hide"></i>
|
||||
{{style.label}}
|
||||
</a>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="unstyled list-icons">
|
||||
<li>
|
||||
<i class="icon icon-add turquoise"></i>
|
||||
<a href="#" ng-click="editStyles()" prevent-default>
|
||||
<localize key="general_edit" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</umb-control-group>
|
||||
</div>
|
||||
|
||||
<umb-overlay
|
||||
ng-if="layoutConfigOverlay.show"
|
||||
model="layoutConfigOverlay"
|
||||
view="layoutConfigOverlay.view"
|
||||
position="right">
|
||||
</umb-overlay>
|
||||
|
||||
<umb-overlay
|
||||
ng-if="rowConfigOverlay.show"
|
||||
model="rowConfigOverlay"
|
||||
view="rowConfigOverlay.view"
|
||||
position="right">
|
||||
</umb-overlay>
|
||||
|
||||
<umb-overlay
|
||||
ng-if="editConfigCollectionOverlay.show"
|
||||
model="editConfigCollectionOverlay"
|
||||
view="editConfigCollectionOverlay.view"
|
||||
position="right">
|
||||
</umb-overlay>
|
||||
|
||||
<umb-overlay
|
||||
ng-if="rowDeleteOverlay.show"
|
||||
model="rowDeleteOverlay"
|
||||
view="rowDeleteOverlay.view"
|
||||
position="right">
|
||||
</umb-overlay>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,900 @@
|
||||
/*
|
||||
Events:
|
||||
grid.initializing
|
||||
grid.initialized
|
||||
grid.rowAdded
|
||||
grid.itemAdded
|
||||
*/
|
||||
angular.module("umbraco")
|
||||
.controller("Umbraco.PropertyEditors.Grid2Controller",
|
||||
function (
|
||||
$scope,
|
||||
$http,
|
||||
assetsService,
|
||||
localizationService,
|
||||
$rootScope,
|
||||
dialogService,
|
||||
gridResource,
|
||||
mediaResource,
|
||||
imageHelper,
|
||||
$timeout,
|
||||
umbRequestHelper,
|
||||
angularHelper,
|
||||
umbDataFormatter,
|
||||
eventsService
|
||||
) {
|
||||
|
||||
// Grid status variables
|
||||
var placeHolder = "";
|
||||
var currentForm = angularHelper.getCurrentForm($scope);
|
||||
|
||||
$scope.currentRow = null;
|
||||
$scope.currentCell = null;
|
||||
$scope.currentToolsControl = null;
|
||||
$scope.currentControl = null;
|
||||
$scope.openRTEToolbarId = null;
|
||||
$scope.hasSettings = false;
|
||||
$scope.showRowConfigurations = true;
|
||||
$scope.sortMode = false;
|
||||
$scope.reorderKey = "general_reorder";
|
||||
|
||||
// *********************************************
|
||||
// Sortable options
|
||||
// *********************************************
|
||||
|
||||
var draggedRteSettings;
|
||||
|
||||
$scope.sortableOptionsRow = {
|
||||
distance: 10,
|
||||
cursor: "move",
|
||||
placeholder: "ui-sortable-placeholder",
|
||||
handle: ".umb-row-title-bar",
|
||||
helper: "clone",
|
||||
forcePlaceholderSize: true,
|
||||
tolerance: "pointer",
|
||||
zIndex: 999999999999999999,
|
||||
scrollSensitivity: 100,
|
||||
cursorAt: {
|
||||
top: 40,
|
||||
left: 60
|
||||
},
|
||||
|
||||
sort: function (event, ui) {
|
||||
/* prevent vertical scroll out of the screen */
|
||||
var max = $(".umb-grid").width() - 150;
|
||||
if (parseInt(ui.helper.css("left")) > max) {
|
||||
ui.helper.css({ "left": max + "px" });
|
||||
}
|
||||
if (parseInt(ui.helper.css("left")) < 20) {
|
||||
ui.helper.css({ "left": 20 });
|
||||
}
|
||||
},
|
||||
|
||||
start: function (e, ui) {
|
||||
|
||||
// Fade out row when sorting
|
||||
ui.item.context.style.display = "block";
|
||||
ui.item.context.style.opacity = "0.5";
|
||||
|
||||
draggedRteSettings = {};
|
||||
ui.item.find(".mceNoEditor").each(function () {
|
||||
// remove all RTEs in the dragged row and save their settings
|
||||
var id = $(this).attr("id");
|
||||
draggedRteSettings[id] = _.findWhere(tinyMCE.editors, { id: id }).settings;
|
||||
// tinyMCE.execCommand("mceRemoveEditor", false, id);
|
||||
});
|
||||
},
|
||||
|
||||
stop: function (e, ui) {
|
||||
|
||||
// Fade in row when sorting stops
|
||||
ui.item.context.style.opacity = "1";
|
||||
|
||||
// reset all RTEs affected by the dragging
|
||||
ui.item.parents(".umb-column").find(".mceNoEditor").each(function () {
|
||||
var id = $(this).attr("id");
|
||||
draggedRteSettings[id] = draggedRteSettings[id] || _.findWhere(tinyMCE.editors, { id: id }).settings;
|
||||
tinyMCE.execCommand("mceRemoveEditor", false, id);
|
||||
tinyMCE.init(draggedRteSettings[id]);
|
||||
});
|
||||
currentForm.$setDirty();
|
||||
}
|
||||
};
|
||||
|
||||
var notIncludedRte = [];
|
||||
var cancelMove = false;
|
||||
var startingCell;
|
||||
|
||||
$scope.sortableOptionsCell = {
|
||||
distance: 10,
|
||||
cursor: "move",
|
||||
placeholder: "ui-sortable-placeholder",
|
||||
handle: ".umb-control-handle",
|
||||
helper: "clone",
|
||||
connectWith: ".umb-cell-inner",
|
||||
forcePlaceholderSize: true,
|
||||
tolerance: "pointer",
|
||||
zIndex: 999999999999999999,
|
||||
scrollSensitivity: 100,
|
||||
cursorAt: {
|
||||
top: 45,
|
||||
left: 90
|
||||
},
|
||||
|
||||
sort: function (event, ui) {
|
||||
|
||||
/* prevent vertical scroll out of the screen */
|
||||
var position = parseInt(ui.item.parent().offset().left) + parseInt(ui.helper.css("left")) - parseInt($(".umb-grid").offset().left);
|
||||
var max = $(".umb-grid").width() - 220;
|
||||
if (position > max) {
|
||||
ui.helper.css({ "left": max - parseInt(ui.item.parent().offset().left) + parseInt($(".umb-grid").offset().left) + "px" });
|
||||
}
|
||||
if (position < 0) {
|
||||
ui.helper.css({ "left": 0 - parseInt(ui.item.parent().offset().left) + parseInt($(".umb-grid").offset().left) + "px" });
|
||||
}
|
||||
},
|
||||
|
||||
over: function (event, ui) {
|
||||
var cell = $(event.target).scope().cell;
|
||||
var allowedEditors = cell.allowed;
|
||||
|
||||
if (($.inArray(ui.item.scope().control.editor.alias, allowedEditors) < 0 && allowedEditors) ||
|
||||
(startingCell != cell && cell.maxItems != '' && cell.maxItems > 0 && cell.maxItems < cell.items.length + 1)) {
|
||||
|
||||
$scope.$apply(function () {
|
||||
$(event.target).scope().cell.dropNotAllowed = true;
|
||||
});
|
||||
|
||||
ui.placeholder.hide();
|
||||
cancelMove = true;
|
||||
}
|
||||
else {
|
||||
if ($(event.target).scope().cell.items.length == 0){
|
||||
|
||||
$scope.$apply(function () {
|
||||
$(event.target).scope().cell.dropOnEmpty = true;
|
||||
});
|
||||
ui.placeholder.hide();
|
||||
} else {
|
||||
ui.placeholder.show();
|
||||
}
|
||||
cancelMove = false;
|
||||
}
|
||||
},
|
||||
|
||||
out: function(event, ui) {
|
||||
$scope.$apply(function () {
|
||||
$(event.target).scope().cell.dropNotAllowed = false;
|
||||
$(event.target).scope().cell.dropOnEmpty = false;
|
||||
});
|
||||
},
|
||||
|
||||
update: function (event, ui) {
|
||||
/* add all RTEs which are affected by the dragging */
|
||||
if (!ui.sender) {
|
||||
if (cancelMove) {
|
||||
ui.item.sortable.cancel();
|
||||
}
|
||||
ui.item.parents(".umb-cell.content").find(".mceNoEditor").each(function () {
|
||||
if ($.inArray($(this).attr("id"), notIncludedRte) < 0) {
|
||||
notIncludedRte.splice(0, 0, $(this).attr("id"));
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
$(event.target).find(".mceNoEditor").each(function () {
|
||||
if ($.inArray($(this).attr("id"), notIncludedRte) < 0) {
|
||||
notIncludedRte.splice(0, 0, $(this).attr("id"));
|
||||
}
|
||||
});
|
||||
}
|
||||
currentForm.$setDirty();
|
||||
},
|
||||
|
||||
start: function (e, ui) {
|
||||
|
||||
//Get the starting cell for reference
|
||||
var cell = $(e.target).scope().cell;
|
||||
startingCell = cell;
|
||||
|
||||
// fade out control when sorting
|
||||
ui.item.context.style.display = "block";
|
||||
ui.item.context.style.opacity = "0.5";
|
||||
|
||||
// reset dragged RTE settings in case a RTE isn't dragged
|
||||
draggedRteSettings = undefined;
|
||||
ui.item.context.style.display = "block";
|
||||
ui.item.find(".mceNoEditor").each(function () {
|
||||
notIncludedRte = [];
|
||||
var editors = _.findWhere(tinyMCE.editors, { id: $(this).attr("id") });
|
||||
|
||||
// save the dragged RTE settings
|
||||
if(editors) {
|
||||
draggedRteSettings = editors.settings;
|
||||
|
||||
// remove the dragged RTE
|
||||
tinyMCE.execCommand("mceRemoveEditor", false, $(this).attr("id"));
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
stop: function (e, ui) {
|
||||
|
||||
// Fade in control when sorting stops
|
||||
ui.item.context.style.opacity = "1";
|
||||
|
||||
ui.item.parents(".umb-cell-content").find(".mceNoEditor").each(function () {
|
||||
if ($.inArray($(this).attr("id"), notIncludedRte) < 0) {
|
||||
// add all dragged's neighbouring RTEs in the new cell
|
||||
notIncludedRte.splice(0, 0, $(this).attr("id"));
|
||||
}
|
||||
});
|
||||
$timeout(function () {
|
||||
// reconstruct the dragged RTE (could be undefined when dragging something else than RTE)
|
||||
if (draggedRteSettings !== undefined) {
|
||||
tinyMCE.init(draggedRteSettings);
|
||||
}
|
||||
|
||||
_.forEach(notIncludedRte, function (id) {
|
||||
// reset all the other RTEs
|
||||
if (draggedRteSettings === undefined || id !== draggedRteSettings.id) {
|
||||
var rteSettings = _.findWhere(tinyMCE.editors, { id: id }).settings;
|
||||
tinyMCE.execCommand("mceRemoveEditor", false, id);
|
||||
tinyMCE.init(rteSettings);
|
||||
}
|
||||
});
|
||||
}, 500, false);
|
||||
|
||||
$scope.$apply(function () {
|
||||
|
||||
var cell = $(e.target).scope().cell;
|
||||
cell.hasActiveChild = hasActiveChild(cell, cell.items);
|
||||
cell.active = false;
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
$scope.toggleSortMode = function() {
|
||||
$scope.sortMode = !$scope.sortMode;
|
||||
if($scope.sortMode) {
|
||||
$scope.reorderKey = "general_reorderDone";
|
||||
} else {
|
||||
$scope.reorderKey = "general_reorder";
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showReorderButton = function() {
|
||||
if($scope.model.value && $scope.model.value.sections) {
|
||||
for(var i = 0; $scope.model.value.sections.length > i; i++) {
|
||||
var section = $scope.model.value.sections[i];
|
||||
if(section.rows && section.rows.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// *********************************************
|
||||
// Add items overlay menu
|
||||
// *********************************************
|
||||
$scope.openEditorOverlay = function (event, cell, index, key) {
|
||||
$scope.editorOverlay = {
|
||||
view: "itempicker",
|
||||
filter: cell.$allowedEditors.length > 15,
|
||||
title: localizationService.localize("grid_insertControl"),
|
||||
availableItems: cell.$allowedEditors,
|
||||
event: event,
|
||||
show: true,
|
||||
submit: function(model) {
|
||||
$scope.addControl(model.selectedItem, cell, index);
|
||||
$scope.editorOverlay.show = false;
|
||||
$scope.editorOverlay = null;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// *********************************************
|
||||
// Row management function
|
||||
// *********************************************
|
||||
|
||||
$scope.clickRow = function(index, rows) {
|
||||
rows[index].active = true;
|
||||
};
|
||||
|
||||
$scope.clickOutsideRow = function(index, rows) {
|
||||
rows[index].active = false;
|
||||
};
|
||||
|
||||
function getAllowedRowLayouts(section) {
|
||||
|
||||
var rowLayouts = $scope.model.config.items.layouts;
|
||||
|
||||
// fixme - remove when refactored datatype
|
||||
_.forEach(rowLayouts, function(rowLayout) {
|
||||
if (!rowLayout.cells) {
|
||||
rowLayout.cells = rowLayout.areas;
|
||||
delete rowLayout.areas;
|
||||
}
|
||||
});
|
||||
|
||||
return rowLayouts;
|
||||
|
||||
//fixme - this is probably not necessary any more
|
||||
|
||||
//This will occur if it is a new section which has been
|
||||
// created from a 'template'
|
||||
if (section.allowed && section.allowed.length > 0) {
|
||||
return _.filter(rowLayouts, function (layout) {
|
||||
return _.indexOf(section.allowed, layout.name) >= 0;
|
||||
});
|
||||
}
|
||||
else {
|
||||
|
||||
|
||||
return rowLayouts;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.addRow = function (section, rowLayout) {
|
||||
|
||||
//copy the selected layout into the rows collection
|
||||
var row = angular.copy(rowLayout);
|
||||
|
||||
//fixme - we've got discrepancies between alias/name on row layouts and rows.
|
||||
row.alias = row.name;
|
||||
|
||||
// Init row value
|
||||
row = initRow(row);
|
||||
|
||||
// Push the new row
|
||||
if (row) {
|
||||
section.rows.push(row);
|
||||
}
|
||||
|
||||
currentForm.$setDirty();
|
||||
|
||||
$scope.showRowConfigurations = false;
|
||||
|
||||
eventsService.emit("grid.rowAdded", { el: $scope.$el, scope: $scope, row: row });
|
||||
|
||||
};
|
||||
|
||||
$scope.removeRow = function (section, $index) {
|
||||
if (section.rows.length > 0) {
|
||||
section.rows.splice($index, 1);
|
||||
$scope.currentRow = null;
|
||||
$scope.openRTEToolbarId = null;
|
||||
currentForm.$setDirty();
|
||||
}
|
||||
|
||||
if(section.rows.length === 0) {
|
||||
$scope.showRowConfigurations = true;
|
||||
}
|
||||
};
|
||||
|
||||
var shouldApply = function(item, itemType, gridItem) {
|
||||
if (item.applyTo === undefined || item.applyTo === null || item.applyTo === "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof (item.applyTo) === "string") {
|
||||
return item.applyTo === itemType;
|
||||
}
|
||||
|
||||
if (itemType === "row") {
|
||||
if (item.applyTo.row === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (item.applyTo.row === null || item.applyTo.row === "") {
|
||||
return true;
|
||||
}
|
||||
var rows = item.applyTo.row.split(',');
|
||||
return _.indexOf(rows, gridItem.name) !== -1;
|
||||
} else if (itemType === "cell") {
|
||||
if (item.applyTo.cell === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (item.applyTo.cell === null || item.applyTo.cell === "") {
|
||||
return true;
|
||||
}
|
||||
var cells = item.applyTo.cell.split(',');
|
||||
var cellSize = gridItem.grid.toString();
|
||||
return _.indexOf(cells, cellSize) !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.editGridItemSettings = function (gridItem, itemType) {
|
||||
|
||||
placeHolder = "{0}";
|
||||
|
||||
var styles, config;
|
||||
if (itemType === 'control') {
|
||||
styles = null;
|
||||
config = angular.copy(gridItem.editor.config.settings);
|
||||
} else {
|
||||
styles = _.filter(angular.copy($scope.model.config.items.styles), function (item) { return shouldApply(item, itemType, gridItem); });
|
||||
config = _.filter(angular.copy($scope.model.config.items.config), function (item) { return shouldApply(item, itemType, gridItem); });
|
||||
}
|
||||
|
||||
if(angular.isObject(gridItem.config)){
|
||||
_.each(config, function(cfg){
|
||||
var val = gridItem.config[cfg.key];
|
||||
if(val){
|
||||
cfg.value = stripModifier(val, cfg.modifier);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(angular.isObject(gridItem.styles)){
|
||||
_.each(styles, function(style){
|
||||
var val = gridItem.styles[style.key];
|
||||
if(val){
|
||||
style.value = stripModifier(val, style.modifier);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.gridItemSettingsDialog = {};
|
||||
$scope.gridItemSettingsDialog.view = "views/propertyeditors/grid/dialogs/config.html";
|
||||
$scope.gridItemSettingsDialog.title = "Settings";
|
||||
$scope.gridItemSettingsDialog.styles = styles;
|
||||
$scope.gridItemSettingsDialog.config = config;
|
||||
|
||||
$scope.gridItemSettingsDialog.show = true;
|
||||
|
||||
$scope.gridItemSettingsDialog.submit = function(model) {
|
||||
|
||||
var styleObject = {};
|
||||
var configObject = {};
|
||||
|
||||
_.each(model.styles, function(style){
|
||||
if(style.value){
|
||||
styleObject[style.key] = addModifier(style.value, style.modifier);
|
||||
}
|
||||
});
|
||||
_.each(model.config, function (cfg) {
|
||||
if (cfg.value) {
|
||||
configObject[cfg.key] = addModifier(cfg.value, cfg.modifier);
|
||||
}
|
||||
});
|
||||
|
||||
gridItem.styles = styleObject;
|
||||
gridItem.config = configObject;
|
||||
gridItem.hasConfig = gridItemHasConfig(styleObject, configObject);
|
||||
|
||||
currentForm.$setDirty();
|
||||
|
||||
$scope.gridItemSettingsDialog.show = false;
|
||||
$scope.gridItemSettingsDialog = null;
|
||||
};
|
||||
|
||||
$scope.gridItemSettingsDialog.close = function(oldModel) {
|
||||
$scope.gridItemSettingsDialog.show = false;
|
||||
$scope.gridItemSettingsDialog = null;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
function stripModifier(val, modifier) {
|
||||
if (!val || !modifier || modifier.indexOf(placeHolder) < 0) {
|
||||
return val;
|
||||
} else {
|
||||
var paddArray = modifier.split(placeHolder);
|
||||
if(paddArray.length == 1){
|
||||
if (modifier.indexOf(placeHolder) === 0) {
|
||||
return val.slice(0, -paddArray[0].length);
|
||||
} else {
|
||||
return val.slice(paddArray[0].length, 0);
|
||||
}
|
||||
} else {
|
||||
if (paddArray[1].length === 0) {
|
||||
return val.slice(paddArray[0].length);
|
||||
}
|
||||
return val.slice(paddArray[0].length, -paddArray[1].length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var addModifier = function(val, modifier){
|
||||
if (!modifier || modifier.indexOf(placeHolder) < 0) {
|
||||
return val;
|
||||
} else {
|
||||
return modifier.replace(placeHolder, val);
|
||||
}
|
||||
};
|
||||
|
||||
function gridItemHasConfig(styles, config) {
|
||||
|
||||
if(_.isEmpty(styles) && _.isEmpty(config)) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// *********************************************
|
||||
// cell management functions
|
||||
// *********************************************
|
||||
|
||||
$scope.clickCell = function(index, cells, row) {
|
||||
cells[index].active = true;
|
||||
row.hasActiveChild = true;
|
||||
};
|
||||
|
||||
$scope.clickOutsideCell = function(index, cells, row) {
|
||||
cells[index].active = false;
|
||||
row.hasActiveChild = hasActiveChild(row, cells);
|
||||
};
|
||||
|
||||
$scope.cellPreview = function (cell) {
|
||||
if (cell && cell.$allowedEditors) {
|
||||
var editor = cell.$allowedEditors[0];
|
||||
return editor.icon;
|
||||
} else {
|
||||
return "icon-layout";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// *********************************************
|
||||
// Control management functions
|
||||
// *********************************************
|
||||
$scope.clickControl = function (index, items, cell) {
|
||||
items[index].active = true;
|
||||
cell.hasActiveChild = true;
|
||||
};
|
||||
|
||||
$scope.clickOutsideControl = function (index, items, cell) {
|
||||
items[index].active = false;
|
||||
cell.hasActiveChild = hasActiveChild(cell, items);
|
||||
};
|
||||
|
||||
function hasActiveChild(item, children) {
|
||||
|
||||
var activeChild = false;
|
||||
|
||||
for(var i = 0; children.length > i; i++) {
|
||||
var child = children[i];
|
||||
|
||||
if(child.active) {
|
||||
activeChild = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(activeChild) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
var guid = (function () {
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
return function () {
|
||||
return s4() + s4() + "-" + s4() + "-" + s4() + "-" +
|
||||
s4() + "-" + s4() + s4() + s4();
|
||||
};
|
||||
})();
|
||||
|
||||
$scope.addControl = function (editor, cell, index, initialize) {
|
||||
|
||||
initialize = (initialize !== false);
|
||||
|
||||
var newItem = {
|
||||
type: editor.udi,
|
||||
values: {},
|
||||
//editor: editor,
|
||||
$initializing: initialize
|
||||
};
|
||||
|
||||
if (index === undefined) {
|
||||
index = cell.items.length;
|
||||
}
|
||||
|
||||
newItem.active = true;
|
||||
|
||||
//populate control
|
||||
initItem(newItem, index + 1);
|
||||
|
||||
cell.items.push(newItem);
|
||||
|
||||
eventsService.emit("grid.itemAdded", { el: $scope.$el, scope: $scope, cell: cell, item: newItem });
|
||||
|
||||
};
|
||||
|
||||
$scope.addTinyMce = function (cell) {
|
||||
var rte = getEditor("rte");
|
||||
$scope.addControl(rte, cell);
|
||||
};
|
||||
|
||||
function getEditor(alias) {
|
||||
return _.find($scope.availableEditors, function (editor) { return editor.alias === alias; });
|
||||
}
|
||||
|
||||
function getEditorByUdi(udi) {
|
||||
return _.find($scope.availableEditors, function (editor) { return editor.udi === udi; });
|
||||
}
|
||||
|
||||
$scope.removeControl = function (cell, $index) {
|
||||
$scope.currentControl = null;
|
||||
cell.items.splice($index, 1);
|
||||
};
|
||||
|
||||
$scope.percentage = function (spans) {
|
||||
return ((spans / $scope.model.config.items.columns) * 100).toFixed(8);
|
||||
};
|
||||
|
||||
|
||||
$scope.clearPrompt = function (scopedObject, e) {
|
||||
scopedObject.deletePrompt = false;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
$scope.togglePrompt = function (scopedObject) {
|
||||
scopedObject.deletePrompt = !scopedObject.deletePrompt;
|
||||
};
|
||||
|
||||
$scope.hidePrompt = function (scopedObject) {
|
||||
scopedObject.deletePrompt = false;
|
||||
};
|
||||
|
||||
$scope.toggleAddRow = function() {
|
||||
$scope.showRowConfigurations = !$scope.showRowConfigurations;
|
||||
};
|
||||
|
||||
|
||||
// *********************************************
|
||||
// Initialization - this runs ONE time
|
||||
// these methods are called from ng-init on the template
|
||||
// so we can controll their first load data
|
||||
//
|
||||
// intialization sets non-saved data like percentage sizing, allowed editors and
|
||||
// other data that should all be pre-fixed with $ to strip it out on save
|
||||
// *********************************************
|
||||
|
||||
// *********************************************
|
||||
// Init template + sections
|
||||
// *********************************************
|
||||
function initContent () {
|
||||
//fixme - config will be different we need to wait for that
|
||||
////settings indicator shortcut
|
||||
//if (($scope.model.config.items.config && $scope.model.config.items.config.length > 0) || ($scope.model.config.items.styles && $scope.model.config.items.styles.length > 0)) {
|
||||
// $scope.hasSettings = true;
|
||||
//}
|
||||
|
||||
//ensure the grid has a column value set, if nothing is found, set it to 12
|
||||
if (!$scope.model.config.items.columns){
|
||||
$scope.model.config.items.columns = 12;
|
||||
} else if (angular.isString($scope.model.config.items.columns)) {
|
||||
$scope.model.config.items.columns = parseInt($scope.model.config.items.columns);
|
||||
}
|
||||
|
||||
//fixme - we are currently still using "layouts" but we will eventually get rid of them. So we need to wrap
|
||||
//our current value with the layout JS since we only persist with rows.
|
||||
var section = {
|
||||
grid: $scope.model.config.items.columns,
|
||||
rows: []
|
||||
};
|
||||
var sectionWrapper = {
|
||||
name: "1 column layout",
|
||||
sections: [
|
||||
section
|
||||
]
|
||||
};
|
||||
|
||||
angular.extend(sectionWrapper.sections[0], $scope.model.value);
|
||||
$scope.model.value = sectionWrapper;
|
||||
|
||||
initSection(section);
|
||||
|
||||
};
|
||||
|
||||
function initSection(section) {
|
||||
section.$percentage = $scope.percentage(section.grid);
|
||||
|
||||
section.allowedRowLayouts = getAllowedRowLayouts(section);
|
||||
|
||||
if (!section.rows || section.rows.length === 0) {
|
||||
section.rows = [];
|
||||
if (section.allowedRowLayouts.length === 1){
|
||||
$scope.addRow(section, section.allowedRowLayouts[0]);
|
||||
}
|
||||
} else {
|
||||
_.forEach(section.rows, function (row, index) {
|
||||
if (!row.$initialized) {
|
||||
var initd = initRow(row);
|
||||
|
||||
//if init fails, remove
|
||||
if (!initd) {
|
||||
section.rows.splice(index, 1);
|
||||
} else {
|
||||
section.rows[index] = initd;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// if there is more than one row added - hide row add tools
|
||||
$scope.showRowConfigurations = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// *********************************************
|
||||
// Init layout / row
|
||||
// *********************************************
|
||||
function initRow (row) {
|
||||
|
||||
//merge the layout data with the original config data
|
||||
//if there are no config info on this, splice it out
|
||||
var original = _.find($scope.model.config.items.layouts, function (o) { return o.name === row.alias; });
|
||||
|
||||
if (!original) {
|
||||
return null;
|
||||
} else {
|
||||
|
||||
//make a copy to not touch the original config
|
||||
original = angular.copy(original);
|
||||
original.styles = row.styles;
|
||||
original.config = row.config;
|
||||
original.hasConfig = gridItemHasConfig(row.styles, row.config);
|
||||
|
||||
|
||||
//sync cell configuration
|
||||
_.each(original.cells, function (cell, cellIndex) {
|
||||
|
||||
|
||||
if (cell.grid > 0) {
|
||||
var currentCell = row.cells[cellIndex];
|
||||
|
||||
if (currentCell) {
|
||||
cell.config = currentCell.config;
|
||||
cell.styles = currentCell.styles;
|
||||
cell.hasConfig = gridItemHasConfig(currentCell.styles, currentCell.config);
|
||||
}
|
||||
|
||||
//set editor permissions
|
||||
if (!cell.allowed || cell.allowAll === true) {
|
||||
cell.$allowedEditors = $scope.availableEditors;
|
||||
cell.$allowsRTE = true;
|
||||
} else {
|
||||
cell.$allowedEditors = _.filter($scope.availableEditors, function (editor) {
|
||||
return _.indexOf(cell.allowed, editor.udi) >= 0;
|
||||
});
|
||||
|
||||
if (_.indexOf(cell.allowed, "rte") >= 0) {
|
||||
cell.$allowsRTE = true;
|
||||
}
|
||||
}
|
||||
|
||||
//copy over existing items into the new cells
|
||||
if (row.cells.length > cellIndex && row.cells[cellIndex].items) {
|
||||
cell.items = currentCell.items;
|
||||
|
||||
_.forEach(cell.items, function (item, itemIndex) {
|
||||
initItem(item, itemIndex);
|
||||
});
|
||||
|
||||
} else {
|
||||
//if empty
|
||||
cell.items = [];
|
||||
|
||||
//if only one allowed editor
|
||||
if (cell.$allowedEditors.length === 1){
|
||||
$scope.addControl(cell.$allowedEditors[0], cell, 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
//set width
|
||||
cell.$percentage = $scope.percentage(cell.grid);
|
||||
cell.$uniqueId = guid();
|
||||
|
||||
} else {
|
||||
original.cells.splice(cellIndex, 1);
|
||||
}
|
||||
});
|
||||
|
||||
//replace the old row
|
||||
original.$initialized = true;
|
||||
|
||||
//set a disposable unique ID
|
||||
original.$uniqueId = guid();
|
||||
|
||||
//set a no disposable unique ID (util for row styling)
|
||||
original.id = !row.id ? guid() : row.id;
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// *********************************************
|
||||
// Init control
|
||||
// *********************************************
|
||||
|
||||
function initItem(item, index) {
|
||||
item.$index = index;
|
||||
item.$uniqueId = guid();
|
||||
|
||||
//create the properties collection which will be bound here
|
||||
item.properties = [];
|
||||
|
||||
//get a scaffold for the current doc type
|
||||
gridResource.getScaffold(item.type).then(function (c) {
|
||||
if (c.tabs && c.tabs.length) {
|
||||
item.properties = c.tabs[0].properties;
|
||||
_.each(item.properties, function (p) {
|
||||
p.$uniqueId = guid();
|
||||
p.hideLabel = true;
|
||||
p.value = item.values[p.alias];
|
||||
|
||||
var editor = getEditorByUdi(item.type);
|
||||
|
||||
//now we need to re-assign the view and set the boolean if it's a preview or not
|
||||
if (editor.views && editor.views[p.alias]) {
|
||||
p.view = editor.views[p.alias].view;
|
||||
}
|
||||
else {
|
||||
//show the icon
|
||||
p.view = "views/propertyeditors/grid2/cellplaceholder.html";
|
||||
p.icon = editor.icon;
|
||||
p.title = editor.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/** Called once to initialize the editor */
|
||||
function init() {
|
||||
gridResource.getGridContentTypes().then(function (response) {
|
||||
$scope.availableEditors = response;
|
||||
|
||||
//Localize the grid editor names
|
||||
angular.forEach($scope.availableEditors, function (value, key) {
|
||||
//If no translation is provided, keep using the editor name from the manifest
|
||||
if (localizationService.dictionary.hasOwnProperty("grid_" + value.alias)) {
|
||||
value.name = localizationService.localize("grid_" + value.alias);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.contentReady = true;
|
||||
|
||||
eventsService.emit("grid.initializing", { el: $scope.$el, scope: $scope });
|
||||
|
||||
//Init grid
|
||||
initContent();
|
||||
|
||||
eventsService.emit("grid.initialized", { el: $scope.$el, scope: $scope });
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
//Clean the grid value before submitting to the server, we don't need
|
||||
// all of that grid configuration in the value to be stored!! All of that
|
||||
// needs to be merged in at runtime to ensure that the real config values are used
|
||||
// if they are ever updated.
|
||||
|
||||
var unsubscribe = $scope.$on("formSubmitting", function () {
|
||||
$scope.model.value = umbDataFormatter.mapGridValueToPersistableModel($scope.model.value);
|
||||
});
|
||||
|
||||
//when the scope is destroyed we need to unsubscribe
|
||||
$scope.$on("$destroy", function () {
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,319 @@
|
||||
<div ng-controller="Umbraco.PropertyEditors.Grid2Controller" class="umb-grid umb-property-editor clearfix" umb-grid-events id="umb-grid">
|
||||
|
||||
<umb-editor-sub-header style="background-color: white;">
|
||||
|
||||
<umb-editor-sub-header-content-right>
|
||||
<umb-button ng-if="showReorderButton()"
|
||||
type="button"
|
||||
icon="icon-navigation"
|
||||
button-style="link"
|
||||
label-key="{{reorderKey}}"
|
||||
action="toggleSortMode()">
|
||||
</umb-button>
|
||||
</umb-editor-sub-header-content-right>
|
||||
|
||||
</umb-editor-sub-header>
|
||||
|
||||
|
||||
|
||||
<div ng-if="contentReady">
|
||||
|
||||
<!-- Template picker -->
|
||||
|
||||
<div class="templates-preview"
|
||||
ng-show="!model.value || model.value == ''">
|
||||
|
||||
<p><strong><localize key="grid_chooseLayout" /></strong></p>
|
||||
|
||||
<div class="preview-rows layout"
|
||||
ng-repeat="template in model.config.items.templates"
|
||||
ng-click="addTemplate(template)">
|
||||
|
||||
<div class="preview-row">
|
||||
|
||||
<div class="preview-col"
|
||||
ng-repeat="section in template.sections"
|
||||
ng-style="{width: percentage(section.grid) + '%'}">
|
||||
|
||||
<div class="preview-cell">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="preview-overlay">
|
||||
</div>
|
||||
|
||||
<small>{{template.name}}</small>
|
||||
|
||||
</div> <!-- .templates-preview-rows -->
|
||||
|
||||
</div> <!-- .templates-preview -->
|
||||
<!-- template picker end -->
|
||||
<!-- Grids -->
|
||||
<div class="umb-grid-width">
|
||||
<div class="tb">
|
||||
|
||||
<!-- for each column in model -->
|
||||
<div class="umb-column td"
|
||||
ng-repeat="section in model.value.sections"
|
||||
ng-init="initSection(section)"
|
||||
ng-style="{width: section.$percentage + '%'}">
|
||||
|
||||
<div ui-sortable="sortableOptionsRow" ng-model="section.rows">
|
||||
|
||||
<!-- for each row in template section -->
|
||||
<!-- ng-mouseenter="setCurrentRow(row)" -->
|
||||
<!-- ng-mouseleave="disableCurrentRow()" -->
|
||||
<div class="umb-row"
|
||||
ng-repeat="row in section.rows"
|
||||
ng-click="clickRow($index, section.rows)"
|
||||
ng-class="{
|
||||
'-has-config': row.hasConfig,
|
||||
'-active': row.active,
|
||||
'-active-child': row.hasActiveChild}"
|
||||
on-outside-click="clickOutsideRow($index, section.rows)"
|
||||
bind-click-on="{{row.active}}"
|
||||
data-rowid="{{row.$uniqueId}}">
|
||||
|
||||
<div class="umb-row-title-bar">
|
||||
|
||||
<div class=".umb-grid-right">
|
||||
<div class="umb-row-title">{{row.label || row.name}}</div>
|
||||
|
||||
<div class="umb-grid-has-config" ng-if="row.hasConfig && !sortMode">
|
||||
<localize key="grid_settingsApplied" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row tool -->
|
||||
<div class="umb-tools row-tools" ng-show="row.active && !sortMode">
|
||||
|
||||
<div class="cell-tools-edit row-tool" ng-if="hasSettings">
|
||||
<i class="icon icon-settings" title="@grid_settings" localize="title" ng-click="editGridItemSettings(row, 'row')"></i>
|
||||
</div>
|
||||
|
||||
<div class="cell-tools-remove row-tool">
|
||||
<i class="icon-trash" ng-click="togglePrompt(row)"></i>
|
||||
<umb-confirm-action ng-if="row.deletePrompt"
|
||||
direction="left"
|
||||
on-confirm="removeRow(section, $index)"
|
||||
on-cancel="hidePrompt(row)">
|
||||
</umb-confirm-action>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- row container -->
|
||||
<div class="{{row.cssClass}} umb-row-inner">
|
||||
|
||||
<div class="mainTb">
|
||||
<div class="tb">
|
||||
<div>
|
||||
|
||||
<!-- Cells in row -->
|
||||
<div class="umb-cell td mainTd"
|
||||
ng-repeat="cell in row.cells"
|
||||
ng-style="{width: cell.$percentage + '%'}"
|
||||
ng-class="{
|
||||
'-has-config': cell.hasConfig,
|
||||
'-active': cell.active,
|
||||
'-active-child': cell.hasActiveChild}"
|
||||
ng-model="cell.items"
|
||||
ng-click="clickCell($index, row.cells, row)"
|
||||
on-outside-click="clickOutsideCell($index, row.cells, row)"
|
||||
bind-click-on="{{cell.active}}">
|
||||
|
||||
<!-- Cell -->
|
||||
<div class="umb-cell-content"
|
||||
ng-class="
|
||||
{'-active': cell.active,
|
||||
'-has-editors' : cell.items.length>
|
||||
0,
|
||||
'-collapsed': sortMode}">
|
||||
|
||||
<!-- disable drop overlay -->
|
||||
<div class="drop-overlay -disable" ng-if="cell.dropNotAllowed">
|
||||
<i class="icon-delete drop-icon"></i>
|
||||
<localize key="grid_contentNotAllowed" />
|
||||
</div>
|
||||
|
||||
<!-- allow drop overlay -->
|
||||
<div class="drop-overlay -allow" ng-if="cell.dropOnEmpty">
|
||||
<i class="icon-download drop-icon"></i>
|
||||
<localize key="grid_contentAllowed" />
|
||||
</div>
|
||||
|
||||
<div class="umb-grid-has-config" ng-if="cell.hasConfig && !sortMode">
|
||||
<localize key="grid_settingsApplied" />
|
||||
</div>
|
||||
|
||||
<div class="cell-tools" ng-if="cell.active && !sortMode">
|
||||
<div class="cell-tool" ng-click="editGridItemSettings(cell, 'cell')">
|
||||
<i class="icon-settings"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="umb-cell-inner" ui-sortable="sortableOptionsCell" ng-model="cell.items">
|
||||
|
||||
<!-- Item placeholder -->
|
||||
<div class="umb-cell-placeholder" ng-if="cell.items.length === 0" ng-click="openEditorOverlay($event, cell, 0, cell.$uniqueId);">
|
||||
<div class="cell-tools-add -center">
|
||||
<localize ng-if="!sortMode" key="grid_addElement" />
|
||||
<localize ng-if="sortMode" key="grid_dropElement" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- for each control in cells -->
|
||||
<div class="umb-control"
|
||||
ng-repeat="item in cell.items"
|
||||
ng-click="clickControl($index, cell.items, cell)"
|
||||
ng-class="{'-active': item.active}"
|
||||
on-outside-click="clickOutsideControl($index, cell.items, cell)"
|
||||
bind-click-on="{{item.active}}"
|
||||
umb-set-dirty-on-change="{{item.value}}"
|
||||
data-itemid="{{item.$uniqueId}}">
|
||||
|
||||
<div class="umb-control-click-overlay" ng-show="!item.active && !sortMode"></div>
|
||||
|
||||
<div class="umb-control-collapsed umb-control-handle" ng-show="sortMode">
|
||||
{{item.editor.name}}
|
||||
</div>
|
||||
|
||||
<div class="umb-control-inner" ng-hide="sortMode">
|
||||
|
||||
<div class="umb-control-bar umb-control-handle">
|
||||
|
||||
<div class="umb-control-title" ng-if="item.active">
|
||||
{{item.editor.name}}
|
||||
</div>
|
||||
|
||||
<div class="umb-tools" ng-if="item.active">
|
||||
|
||||
<div class="umb-control-tool" ng-if="item.editor.config.settings">
|
||||
<i class="umb-control-tool-icon icon-settings" ng-click="editGridItemSettings(item, 'control')"></i>
|
||||
</div>
|
||||
|
||||
<div class="umb-control-tool">
|
||||
<i class="umb-control-tool-icon icon-trash" ng-click="togglePrompt(item)"></i>
|
||||
<umb-confirm-action ng-if="item.deletePrompt"
|
||||
direction="left"
|
||||
on-confirm="removeControl(cell, $index)"
|
||||
on-cancel="hidePrompt(control)">
|
||||
</umb-confirm-action>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Rendering the editor for specific control -->
|
||||
<div ng-if="item">
|
||||
<div ng-repeat="property in item.properties">
|
||||
<umb-property property="property">
|
||||
<umb-property-editor model="property"></umb-property-editor>
|
||||
</umb-property>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- items repeat end -->
|
||||
<!-- if cell is empty tools -->
|
||||
<div class="umb-grid-add-more-content" ng-if="cell.items.length > 0 && !sortMode && (cell.maxItems == undefined || cell.maxItems == '' || cell.maxItems == 0 || cell.maxItems > cell.items.length)">
|
||||
<div class="cell-tools-add -bar newbtn" ng-click="openEditorOverlay($event, cell, 0, cell.$uniqueId);"><localize key="grid_addElement" /></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- cells repeat end -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- row container end -->
|
||||
|
||||
</div>
|
||||
<!-- row repeat -->
|
||||
|
||||
</div>
|
||||
<!-- row sortable end -->
|
||||
<!-- column tools -->
|
||||
|
||||
<div class="umb-add-row" ng-if="!sortMode">
|
||||
|
||||
<a href=""
|
||||
class="iconBox"
|
||||
ng-click="toggleAddRow()"
|
||||
ng-if="!showRowConfigurations">
|
||||
|
||||
<i class=" icon icon-add" title="@general_add" localize="title"></i>
|
||||
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="templates-preview" ng-if="showRowConfigurations">
|
||||
|
||||
<p ng-hide="section.rows.length > 0"><strong><localize key="grid_addRows" /></strong></p>
|
||||
|
||||
<div class="preview-rows columns"
|
||||
ng-repeat="rowLayout in section.allowedRowLayouts"
|
||||
ng-show="rowLayout.cells.length > 0"
|
||||
ng-click="addRow(section, rowLayout)">
|
||||
|
||||
<div class="preview-row">
|
||||
|
||||
<div class="preview-col" ng-style="{width: percentage(cell.grid) + '%'}" ng-repeat="cell in rowLayout.cells">
|
||||
|
||||
<div class="preview-cell">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="preview-overlay">
|
||||
</div>
|
||||
|
||||
<small>{{rowLayout.label || rowLayout.name}}</small>
|
||||
|
||||
</div> <!-- .templates-preview-rows -->
|
||||
|
||||
</div> <!-- .templates-preview -->
|
||||
<!-- column tools end -->
|
||||
|
||||
</div>
|
||||
<!-- column repeat end -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre>
|
||||
<code>{{model.value | json}}</code>
|
||||
</pre>
|
||||
<pre>
|
||||
<code>{{availableEditors | json}}</code>
|
||||
</pre>
|
||||
|
||||
<umb-overlay ng-if="editorOverlay.show"
|
||||
model="editorOverlay"
|
||||
view="editorOverlay.view"
|
||||
position="target">
|
||||
</umb-overlay>
|
||||
|
||||
<umb-overlay ng-if="gridItemSettingsDialog.show"
|
||||
model="gridItemSettingsDialog"
|
||||
view="gridItemSettingsDialog.view"
|
||||
position="right">
|
||||
</umb-overlay>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
<div ng-controller="Umbraco.PropertyEditors.textboxController">
|
||||
<textarea rows="1"
|
||||
umb-auto-resize
|
||||
umb-auto-focus
|
||||
ng-required="model.validation.mandatory"
|
||||
ng-keyup="model.change()"
|
||||
ng-trim="false"
|
||||
class="textstring input-block-level" id="{{model.$uniqueId}}_text"
|
||||
ng-model="model.value"
|
||||
localize="placeholder" placeholder="@grid_placeholderWriteHere"
|
||||
style="font-size: 36px; line-height: 45px; font-weight: bold;"></textarea>
|
||||
</div>
|
||||
@@ -0,0 +1,361 @@
|
||||
/*globals jasmine*/
|
||||
|
||||
describe("grid 2", function () {
|
||||
|
||||
var controller,
|
||||
rootScope,
|
||||
scope,
|
||||
gridResource,
|
||||
angularHelper,
|
||||
q,
|
||||
// These are populated at the bottom of this file
|
||||
gridEditors,
|
||||
rteScaffold,
|
||||
headerScaffold,
|
||||
scaffolds,
|
||||
fullModel;
|
||||
|
||||
beforeEach(module('umbraco'));
|
||||
|
||||
beforeEach(inject(function (
|
||||
$rootScope,
|
||||
$controller,
|
||||
$q
|
||||
) {
|
||||
q = $q;
|
||||
rootScope = $rootScope;
|
||||
scope = $rootScope.$new();
|
||||
|
||||
scope.model = JSON.parse(JSON.stringify(fullModel));
|
||||
|
||||
gridResource = {
|
||||
getGridContentTypes: function () {
|
||||
var def = q.defer();
|
||||
def.resolve(gridEditors);
|
||||
return def.promise;
|
||||
},
|
||||
getScaffold: function (uuid) {
|
||||
var def = q.defer();
|
||||
def.resolve(scaffolds[uuid]);
|
||||
return def.promise;
|
||||
}
|
||||
}
|
||||
|
||||
angularHelper = {
|
||||
getCurrentForm: function () {
|
||||
return {
|
||||
$setDirty: function () {
|
||||
var iRememberBeingDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
controller = $controller("Umbraco.PropertyEditors.Grid2Controller", {
|
||||
"$scope": scope,
|
||||
"gridResource": gridResource,
|
||||
"angularHelper": angularHelper
|
||||
});
|
||||
|
||||
//fixme - Disable digest while we don't have the right logic
|
||||
scope.$digest();
|
||||
}));
|
||||
|
||||
it("defaults to 12 columns", function () {
|
||||
expect(scope.model.config.items.columns).toBe(12);
|
||||
});
|
||||
|
||||
it("when only one layout and row config, adds layout and row", function () {
|
||||
expect(scope.model.value.sections[0].rows[0]).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows add editor dialog", function () {
|
||||
expect(scope.editorOverlay).toBeUndefined();
|
||||
scope.openEditorOverlay(
|
||||
{},
|
||||
scope.model.value.sections[0].rows[0].cells[0],
|
||||
0,
|
||||
""
|
||||
);
|
||||
expect(scope.editorOverlay).toEqual(jasmine.objectContaining({
|
||||
view: "itempicker"
|
||||
}));
|
||||
});
|
||||
|
||||
it("adds editor to cell",
|
||||
function () {
|
||||
scope.addControl(
|
||||
gridEditors[0],
|
||||
scope.model.value.sections[0].rows[0].cells[0],
|
||||
0
|
||||
);
|
||||
expect(scope.model.value.sections[0].rows[0].cells[0].items[2]).toEqual(jasmine.objectContaining({
|
||||
"type": "umb://document-type/9c620216c4f14f67b16468b103a09144",
|
||||
"values": {
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
it("maps the model to persistable model",
|
||||
function () {
|
||||
//var persistable = scope.mapToPersistableModel(fullModel.value);
|
||||
|
||||
scope.$emit("formSubmitting");
|
||||
var persisted = scope.model.value;
|
||||
|
||||
expect(persisted).toEqual(jasmine.objectContaining({
|
||||
rows: [
|
||||
{
|
||||
alias: "Headline",
|
||||
settings: {},
|
||||
cells: [
|
||||
{
|
||||
"settings": {},
|
||||
"items": [
|
||||
{
|
||||
"type": "umb://document-type/9c620216c4f14f67b16468b103a09144",
|
||||
"values": {
|
||||
"value": "Hello world!"
|
||||
}
|
||||
}, {
|
||||
"type": "umb://document-type/437c65f147054681911d8a884459ab47",
|
||||
"values": {
|
||||
"content": "<p>More hello world!</p>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
gridEditors = [
|
||||
{
|
||||
"id": 1061,
|
||||
"udi": "umb://document-type/9c620216c4f14f67b16468b103a09144",
|
||||
"name": "Header",
|
||||
"alias": "header",
|
||||
"icon": "icon-document",
|
||||
"views": {
|
||||
"value": {
|
||||
"view": "views/propertyeditors/textbox/textbox.inline.html",
|
||||
"isPreview": false
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"id": 1062,
|
||||
"udi": "umb://document-type/437c65f147054681911d8a884459ab47",
|
||||
"name": "RTE",
|
||||
"alias": "richText",
|
||||
"icon": "icon-document",
|
||||
"views": {
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
rteScaffold = {
|
||||
"tabs": [
|
||||
{
|
||||
"id": 13,
|
||||
"active": true,
|
||||
"label": "Inline",
|
||||
"alias": "Inline",
|
||||
"properties": [
|
||||
{
|
||||
"label": "Content",
|
||||
"description": null,
|
||||
"view": "rte",
|
||||
"config": {
|
||||
"editor": null,
|
||||
"hideLabel": false
|
||||
},
|
||||
"hideLabel": false,
|
||||
"validation": {
|
||||
"mandatory": false,
|
||||
"pattern": null
|
||||
},
|
||||
"readonly": false,
|
||||
"id": 0,
|
||||
"value": null,
|
||||
"alias": "content",
|
||||
"editor": "Umbraco.TinyMCEv3",
|
||||
"isSensitive": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"updateDate": "0001-01-01T00:00:00",
|
||||
"createDate": "0001-01-01T00:00:00",
|
||||
"published": false,
|
||||
"edited": false,
|
||||
"owner": null,
|
||||
"updater": null,
|
||||
"contentTypeAlias": "richText",
|
||||
"sortOrder": 0,
|
||||
"name": null,
|
||||
"id": 0,
|
||||
"udi": "umb://document/437c65f147054681911d8a884459ab47",
|
||||
"icon": "icon-document",
|
||||
"trashed": false,
|
||||
"key": "437c65f1-4705-4681-911d-8a884459ab47",
|
||||
"parentId": -1,
|
||||
"alias": null,
|
||||
"path": null,
|
||||
"metaData": {}
|
||||
};
|
||||
|
||||
headerScaffold = {
|
||||
"tabs": [
|
||||
{
|
||||
"id": 12,
|
||||
"active": true,
|
||||
"label": "Inline",
|
||||
"alias": "Inline",
|
||||
"properties": [
|
||||
{
|
||||
"label": "Value",
|
||||
"description": null,
|
||||
"view": "textbox",
|
||||
"config": {
|
||||
"maxChars": null
|
||||
},
|
||||
"hideLabel": false,
|
||||
"validation": {
|
||||
"mandatory": false,
|
||||
"pattern": null
|
||||
},
|
||||
"readonly": false,
|
||||
"id": 0,
|
||||
"value": "",
|
||||
"alias": "value",
|
||||
"editor": "Umbraco.TextBox",
|
||||
"isSensitive": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"updateDate": "0001-01-01T00:00:00",
|
||||
"createDate": "0001-01-01T00:00:00",
|
||||
"published": false,
|
||||
"edited": false,
|
||||
"owner": null,
|
||||
"updater": null,
|
||||
"contentTypeAlias": "header",
|
||||
"sortOrder": 0,
|
||||
"name": null,
|
||||
"id": 0,
|
||||
"udi": "umb://document/9c620216c4f14f67b16468b103a09144",
|
||||
"icon": "icon-document",
|
||||
"trashed": false,
|
||||
"key": "1e03aad06a124e9e8092540709d3c1cf",
|
||||
"parentId": -1,
|
||||
"alias": null,
|
||||
"path": null,
|
||||
"metaData": {}
|
||||
};
|
||||
|
||||
scaffolds = {
|
||||
"umb://document-type/437c65f147054681911d8a884459ab47": rteScaffold,
|
||||
"umb://document-type/9c620216c4f14f67b16468b103a09144": headerScaffold
|
||||
};
|
||||
|
||||
fullModel = {
|
||||
"label": "Layout",
|
||||
"description": null,
|
||||
"view": "grid2",
|
||||
"config": {
|
||||
"items": {
|
||||
"styles": [
|
||||
{
|
||||
"label": "Set a background image",
|
||||
"description": "Set a row background",
|
||||
"key": "background-image",
|
||||
"view": "imagepicker",
|
||||
"modifier": "url({0})"
|
||||
}
|
||||
],
|
||||
"config": [
|
||||
{
|
||||
"label": "Class",
|
||||
"description": "Set a css class",
|
||||
"key": "class",
|
||||
"view": "textstring"
|
||||
}
|
||||
],
|
||||
"columns": 12,
|
||||
"templates": [
|
||||
{
|
||||
"name": "1 column layout",
|
||||
"sections": [
|
||||
{
|
||||
"grid": 12
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"layouts": [
|
||||
{
|
||||
"label": "Headline",
|
||||
"name": "Headline",
|
||||
"areas": [
|
||||
{
|
||||
"grid": 12,
|
||||
"allowed": [
|
||||
"umb://document-type/437c65f147054681911d8a884459ab47",
|
||||
"umb://document-type/9c620216c4f14f67b16468b103a09144"
|
||||
]
|
||||
}
|
||||
]
|
||||
}, {
|
||||
"label": "FiftyFifty",
|
||||
"name": "Article",
|
||||
"areas": [
|
||||
{
|
||||
"grid": 6
|
||||
}, {
|
||||
"grid": 6
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"hideLabel": true,
|
||||
"validation": {
|
||||
"mandatory": false,
|
||||
"pattern": null
|
||||
},
|
||||
"readonly": false,
|
||||
"id": 0,
|
||||
"value": {
|
||||
"rows": [
|
||||
{
|
||||
"alias": "Headline",
|
||||
"settings": {},
|
||||
"cells": [
|
||||
{
|
||||
"settings": {},
|
||||
"items": [
|
||||
{
|
||||
"type": "umb://document-type/9c620216c4f14f67b16468b103a09144",
|
||||
"values": {
|
||||
"value": "Hello world!"
|
||||
}
|
||||
}, {
|
||||
"type": "umb://document-type/437c65f147054681911d8a884459ab47",
|
||||
"values": {
|
||||
"content": "<p>More hello world!</p>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}]
|
||||
}]
|
||||
},
|
||||
"alias": "layout",
|
||||
"editor": "Umbraco.Grid2",
|
||||
"isSensitive": false
|
||||
};
|
||||
});
|
||||
@@ -109,6 +109,7 @@
|
||||
<PackageReference Include="MySql.Data" Version="6.10.7" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||
<PackageReference Include="Umbraco.SqlServerCE" Version="4.0.0.1" />
|
||||
<PackageReference Include="Umbraco.ModelsBuilder" Version="8.0.0-alpha.18" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj">
|
||||
@@ -119,10 +120,6 @@
|
||||
<Name>Umbraco.Examine</Name>
|
||||
<Project>{07FBC26B-2927-4A22-8D96-D644C667FECC}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Umbraco.ModelsBuilder\Umbraco.ModelsBuilder.csproj">
|
||||
<Project>{7020a059-c0d1-43a0-8efd-23591a0c9af6}</Project>
|
||||
<Name>Umbraco.ModelsBuilder</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Umbraco.Web\Umbraco.Web.csproj">
|
||||
<Project>{651e1350-91b6-44b7-bd60-7207006d7003}</Project>
|
||||
<Name>Umbraco.Web</Name>
|
||||
@@ -489,6 +486,13 @@
|
||||
<Content Include="Umbraco\Views\Preview\Shadow.cshtml" />
|
||||
<Content Include="Umbraco\Views\Preview\Slider.cshtml" />
|
||||
<Content Include="Umbraco\Views\Preview\web.config" />
|
||||
<Content Include="Views\Partials\Grid2\Bootstrap4.cshtml" />
|
||||
<Content Include="Views\Partials\Grid2\Editors\Embed.cshtml" />
|
||||
<Content Include="Views\Partials\Grid2\Editors\Macro.cshtml" />
|
||||
<Content Include="Views\Partials\Grid2\Editors\Media.cshtml" />
|
||||
<Content Include="Views\Partials\Grid2\Editors\Rte.cshtml" />
|
||||
<Content Include="Views\Partials\Grid2\Editors\TextString.cshtml" />
|
||||
<Content Include="Views\Home.cshtml" />
|
||||
<None Include="Web.Debug.config.transformed" />
|
||||
<None Include="web.Template.Debug.config">
|
||||
<DependentUpon>Web.Template.config</DependentUpon>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
@inherits UmbracoViewPage<Grid2Value>
|
||||
@if (Model != null && Model.Rows != null)
|
||||
{
|
||||
<div class="umb-grid2">
|
||||
<div class="container">
|
||||
@foreach (var row in Model.Rows)
|
||||
{
|
||||
<div @RenderElementAttributes(row.Settings, "row")>
|
||||
@foreach (var cell in row.Cells)
|
||||
{
|
||||
<div @RenderElementAttributes(cell.Settings, $"col-{cell.Colspan}")>
|
||||
@foreach (var item in cell.Items)
|
||||
{
|
||||
try
|
||||
{
|
||||
@Html.Partial($"Grid2/Editors/{item.ContentType.Alias}", item)
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
<pre>@ex.ToString()</pre>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@functions {
|
||||
public static MvcHtmlString RenderElementAttributes(IPublishedElement settings, string className)
|
||||
{
|
||||
/* NOTE: Developers will need to implement their own attributes based on their Settings document type */
|
||||
|
||||
var items = new Dictionary<string, string>();
|
||||
|
||||
// loop over each of the properties
|
||||
if (settings != null)
|
||||
{
|
||||
foreach (var property in settings.Properties)
|
||||
{
|
||||
var propertyValue = HttpUtility.HtmlAttributeEncode(property.Value<string>());
|
||||
items.Add(property.Alias, propertyValue);
|
||||
}
|
||||
}
|
||||
|
||||
// we need to add the `className` (for row/cell),
|
||||
// so we check if the "class" attribute has already been defined.
|
||||
// if it does, then we'll add to it, otherwise we'll create it.
|
||||
if (items.ContainsKey("class"))
|
||||
{
|
||||
items["class"] = $"{className} {items["class"]}";
|
||||
}
|
||||
else
|
||||
{
|
||||
items.Add("class", className);
|
||||
}
|
||||
|
||||
var attributes = new List<string>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
attributes.Add($"{item.Key}=\"{item.Value}\"");
|
||||
}
|
||||
|
||||
return new MvcHtmlString(string.Join(" ", attributes));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@inherits UmbracoViewPage<GridValue.GridControl>
|
||||
|
||||
<div class="video-wrapper">
|
||||
@Html.Raw(Model.Value)
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
@inherits UmbracoViewPage<GridValue.GridControl>
|
||||
@if (Model.Value != null)
|
||||
{
|
||||
// TODO: Get the "macroAlias" items
|
||||
var macroAlias = Model.Value.macroAlias.ToString();
|
||||
ViewDataDictionary parameters = new ViewDataDictionary();
|
||||
|
||||
// TODO: Get the "macroParamsDictionary" items
|
||||
foreach (dynamic mpd in Model.value.macroParamsDictionary)
|
||||
{
|
||||
parameters.Add(mpd.Name, mpd.Value);
|
||||
}
|
||||
|
||||
<text>
|
||||
@Umbraco.RenderMacro(macroAlias, parameters)
|
||||
</text>
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
@inherits UmbracoViewPage<GridValue.GridControl>
|
||||
@if (Model.Value != null)
|
||||
{
|
||||
// TODO: Get the "image" value
|
||||
var url = Model.Value.image;
|
||||
if (Model.Editor.Config != null && Model.Editor.Config.ContainsKey("size"))
|
||||
{
|
||||
url += "?width=" + Model.Editor.Config.size.width;
|
||||
url += "&height=" + Model.Editor.Config.size.height;
|
||||
|
||||
// TODO: Get the "focalPoint" value
|
||||
if (Model.Value.focalPoint != null)
|
||||
{
|
||||
url += "¢er=" + Model.Value.focalPoint.top + "," + Model.Value.focalPoint.left;
|
||||
url += "&mode=crop";
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Get the "altText" value
|
||||
var altText = Model.Value.altText ?? Model.Value.caption ?? string.Empty;
|
||||
|
||||
<img src="@url" alt="@altText">
|
||||
|
||||
// TODO: Get the "caption" value
|
||||
if (Model.Value.caption != null)
|
||||
{
|
||||
<p class="caption">@Model.Value.caption</p>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@using Umbraco.Web.Templates
|
||||
@inherits UmbracoViewPage<GridValue.GridControl>
|
||||
|
||||
@Html.Raw(TemplateUtilities.ParseInternalLinks(Model.Value.ToString()))
|
||||
@@ -0,0 +1,23 @@
|
||||
@inherits UmbracoViewPage<GridValue.GridControl>
|
||||
@if (Model.Editor.Config != null && Model.Editor.Config.ContainsKey("markup"))
|
||||
{
|
||||
var markup = Model.Editor.Config["markup"].ToString();
|
||||
markup = markup.Replace("#value#", Umbraco.ReplaceLineBreaksForHtml(HttpUtility.HtmlEncode(Model.Value)));
|
||||
|
||||
if (markup.Contains("#style#") && Model.Editor.Config.ContainsKey("style"))
|
||||
{
|
||||
var style = Model.Editor.Config["style"].ToString();
|
||||
markup = markup.Replace("#style#", style);
|
||||
}
|
||||
|
||||
<text>
|
||||
@Html.Raw(markup)
|
||||
</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
var style = Model.Editor.Config.ContainsKey("style") ? Model.Editor.Config["style"].ToString() : null;
|
||||
<text>
|
||||
<div style="@style">@Model.Value</div>
|
||||
</text>
|
||||
}
|
||||
@@ -292,6 +292,10 @@ namespace Umbraco.Web.Editors
|
||||
{
|
||||
"languageApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<LanguageController>(
|
||||
controller => controller.GetAllLanguages())
|
||||
},
|
||||
{
|
||||
"gridApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<GridController>(
|
||||
controller => controller.GetContentTypes())
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
using AutoMapper;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Web.Http;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Web.Models.ContentEditing;
|
||||
|
||||
namespace Umbraco.Web.Editors
|
||||
{
|
||||
public class GridController : UmbracoAuthorizedJsonController
|
||||
{
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
|
||||
public GridController(PropertyEditorCollection propertyEditors)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
}
|
||||
|
||||
public IEnumerable<GridContentType> GetContentTypes()
|
||||
{
|
||||
var contentTypes = GetGridContentTypes();
|
||||
|
||||
return contentTypes.Select(x => new GridContentType
|
||||
{
|
||||
Id = x.Id,
|
||||
Udi = x.GetUdi(),
|
||||
Name = x.Name,
|
||||
Alias = x.Alias,
|
||||
Icon = x.Icon,
|
||||
Views = GetPaths(x)
|
||||
});
|
||||
}
|
||||
|
||||
public GridContentCell GetScaffold(Udi id)
|
||||
{
|
||||
var guidUdi = id as GuidUdi;
|
||||
if (guidUdi == null)
|
||||
{
|
||||
throw new HttpResponseException(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
|
||||
var contentType = Services.ContentTypeService.Get(guidUdi.Guid);
|
||||
if (contentType == null) throw new HttpResponseException(System.Net.HttpStatusCode.NotFound);
|
||||
|
||||
var emptyContent = Services.ContentService.Create("", -1, contentType.Alias, Security.GetUserId().ResultOr(0));
|
||||
var mapped = Mapper.Map<GridContentCell>(emptyContent);
|
||||
|
||||
//remove this tab if it exists: umbContainerView
|
||||
var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName);
|
||||
mapped.Tabs = mapped.Tabs.Except(new[] { containerTab });
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
public IDictionary<string, GridEditorPath> GetPaths(IContentType contentType)
|
||||
{
|
||||
if (contentType.PropertyGroups.Count == 0)
|
||||
throw new InvalidOperationException($"The content type {contentType.Alias} does not contain any tabs/properties");
|
||||
|
||||
var props = contentType.PropertyGroups[0].PropertyTypes;
|
||||
|
||||
var result = new Dictionary<string, GridEditorPath>();
|
||||
|
||||
var editors = props.Select(x => _propertyEditors.FirstOrDefault(p => p.Alias == x.PropertyEditorAlias))
|
||||
.WhereNotNull()
|
||||
.ToDictionary(x => x.Alias, x => x);
|
||||
|
||||
foreach (var x in props)
|
||||
{
|
||||
if (editors.TryGetValue(x.PropertyEditorAlias, out var editor))
|
||||
{
|
||||
var valueEditor = editor.GetValueEditor();
|
||||
if (valueEditor == null) continue;
|
||||
|
||||
if (valueEditor.View.IsNullOrWhiteSpace()) continue;
|
||||
|
||||
var inlineResult = GetPath(valueEditor, "inline");
|
||||
if (inlineResult)
|
||||
result[x.Alias] = new GridEditorPath(inlineResult.Result, false);
|
||||
else
|
||||
{
|
||||
var previewResult = GetPath(valueEditor, "preview");
|
||||
if (previewResult)
|
||||
result[x.Alias] = new GridEditorPath(previewResult.Result, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private IEnumerable<IContentType> GetGridContentTypes()
|
||||
{
|
||||
const string GridContentTypeFolderName = "GridEditors";
|
||||
|
||||
var folder = Services.ContentTypeService
|
||||
.GetContainers(GridContentTypeFolderName, 1)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (folder == null)
|
||||
{
|
||||
return Enumerable.Empty<IContentType>();
|
||||
}
|
||||
|
||||
var contentTypes = Services.ContentTypeService
|
||||
.GetDescendants(folder.Id, false)
|
||||
.Where(x => !x.IsContainer);
|
||||
|
||||
return contentTypes;
|
||||
}
|
||||
|
||||
private Attempt<string> GetPath(IDataValueEditor valueEditor, string suffix)
|
||||
{
|
||||
var inlinePath = valueEditor.View.InvariantEndsWith(".html")
|
||||
? valueEditor.View.TrimEnd(".html") + $".{suffix}.html"
|
||||
: valueEditor.View + $".{suffix}.html";
|
||||
|
||||
var relativePath = !inlinePath.Contains("/") ? $"views/propertyeditors/{valueEditor.View}/{inlinePath}" : inlinePath;
|
||||
var fullPath = !inlinePath.Contains("/") ? $"~{GlobalSettings.Path}/{relativePath}" : relativePath;
|
||||
|
||||
var file = IOHelper.MapPath(fullPath);
|
||||
|
||||
if (!System.IO.File.Exists(file))
|
||||
return Attempt<string>.Fail();
|
||||
|
||||
return Attempt.Succeed(relativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,151 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web.Mvc.Html;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models;
|
||||
using System.Web.Mvc;
|
||||
using Umbraco.Web.Templates;
|
||||
using System.IO;
|
||||
using System.Web.Routing;
|
||||
using System.Web.Mvc.Html;
|
||||
using Umbraco.Core.Exceptions;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Web.Mvc;
|
||||
|
||||
namespace Umbraco.Web
|
||||
{
|
||||
|
||||
public static class GridTemplateExtensions
|
||||
{
|
||||
public static MvcHtmlString GetGridHtml(this HtmlHelper html, IPublishedProperty property, string framework = "bootstrap3")
|
||||
{
|
||||
var asString = property.GetValue() as string;
|
||||
if (asString != null && string.IsNullOrEmpty(asString)) return new MvcHtmlString(string.Empty);
|
||||
|
||||
var view = "Grid/" + framework;
|
||||
return html.Partial(view, property.GetValue());
|
||||
}
|
||||
private const string _defaultPropertyAlias = "bodyText";
|
||||
private const string _defaultFramework = "bootstrap4";
|
||||
|
||||
public static MvcHtmlString GetGridHtml(this HtmlHelper html, IPublishedContent contentItem)
|
||||
{
|
||||
return html.GetGridHtml(contentItem, "bodyText", "bootstrap3");
|
||||
return html.GetGridHtml(contentItem, _defaultPropertyAlias, _defaultFramework);
|
||||
}
|
||||
|
||||
public static MvcHtmlString GetGridHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentNullOrEmptyException(nameof(propertyAlias));
|
||||
|
||||
return html.GetGridHtml(contentItem, propertyAlias, "bootstrap3");
|
||||
return html.GetGridHtml(contentItem, propertyAlias, _defaultFramework);
|
||||
}
|
||||
|
||||
public static MvcHtmlString GetGridHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias, string framework)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentNullOrEmptyException(nameof(propertyAlias));
|
||||
if (string.IsNullOrWhiteSpace(propertyAlias))
|
||||
throw new ArgumentNullOrEmptyException(nameof(propertyAlias));
|
||||
|
||||
var view = "Grid/" + framework;
|
||||
var prop = contentItem.GetProperty(propertyAlias);
|
||||
if (prop == null) throw new NullReferenceException("No property type found with alias " + propertyAlias);
|
||||
var model = prop.GetValue();
|
||||
var property = contentItem.GetProperty(propertyAlias);
|
||||
if (property == null)
|
||||
throw new NullReferenceException($"No property type found with alias '{propertyAlias}'");
|
||||
|
||||
var asString = model as string;
|
||||
if (asString != null && string.IsNullOrEmpty(asString)) return new MvcHtmlString(string.Empty);
|
||||
|
||||
return html.Partial(view, model);
|
||||
return html.GetGridHtml(property, framework);
|
||||
}
|
||||
|
||||
public static MvcHtmlString GetGridHtml(this IPublishedProperty property, HtmlHelper html, string framework = "bootstrap3")
|
||||
public static MvcHtmlString GetGridHtml(this HtmlHelper html, IPublishedProperty property, string framework = _defaultFramework)
|
||||
{
|
||||
var asString = property.GetValue() as string;
|
||||
if (asString != null && string.IsNullOrEmpty(asString)) return new MvcHtmlString(string.Empty);
|
||||
if (string.IsNullOrWhiteSpace(framework))
|
||||
framework = _defaultFramework;
|
||||
|
||||
var view = "Grid/" + framework;
|
||||
return html.Partial(view, property.GetValue());
|
||||
var model = property.GetValue();
|
||||
|
||||
// NOTE: The Grid v2 uses a strongly-typed model, v1 is dynamic
|
||||
if (model is Grid2Value grid)
|
||||
return html.Partial($"Grid2/{framework}", model);
|
||||
|
||||
if (model is string s && string.IsNullOrEmpty(s))
|
||||
return new MvcHtmlString(string.Empty);
|
||||
|
||||
return html.Partial($"Grid/{framework}", model);
|
||||
}
|
||||
|
||||
public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, HtmlHelper html)
|
||||
{
|
||||
return GetGridHtml(contentItem, html, "bodyText", "bootstrap3");
|
||||
return GetGridHtml(contentItem, html, _defaultPropertyAlias, _defaultFramework);
|
||||
}
|
||||
|
||||
public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentNullOrEmptyException(nameof(propertyAlias));
|
||||
|
||||
return GetGridHtml(contentItem, html, propertyAlias, "bootstrap3");
|
||||
return GetGridHtml(contentItem, html, propertyAlias, _defaultFramework);
|
||||
}
|
||||
|
||||
public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias, string framework)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentNullOrEmptyException(nameof(propertyAlias));
|
||||
|
||||
var view = "Grid/" + framework;
|
||||
var prop = contentItem.GetProperty(propertyAlias);
|
||||
if (prop == null) throw new NullReferenceException("No property type found with alias " + propertyAlias);
|
||||
var model = prop.GetValue();
|
||||
|
||||
var asString = model as string;
|
||||
if (asString != null && string.IsNullOrEmpty(asString)) return new MvcHtmlString(string.Empty);
|
||||
|
||||
return html.Partial(view, model);
|
||||
return html.GetGridHtml(contentItem, propertyAlias, framework);
|
||||
}
|
||||
|
||||
|
||||
[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")]
|
||||
public static MvcHtmlString GetGridHtml(this IPublishedProperty property, string framework = "bootstrap3")
|
||||
public static MvcHtmlString GetGridHtml(this IPublishedProperty property, HtmlHelper html, string framework = _defaultFramework)
|
||||
{
|
||||
var asString = property.GetValue() as string;
|
||||
if (asString != null && string.IsNullOrEmpty(asString)) return new MvcHtmlString(string.Empty);
|
||||
|
||||
var htmlHelper = CreateHtmlHelper(property.GetValue());
|
||||
return htmlHelper.GetGridHtml(property, framework);
|
||||
}
|
||||
|
||||
[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")]
|
||||
public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem)
|
||||
{
|
||||
return GetGridHtml(contentItem, "bodyText", "bootstrap3");
|
||||
}
|
||||
|
||||
[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")]
|
||||
public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, string propertyAlias)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentNullOrEmptyException(nameof(propertyAlias));
|
||||
|
||||
return GetGridHtml(contentItem, propertyAlias, "bootstrap3");
|
||||
}
|
||||
|
||||
[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")]
|
||||
public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, string propertyAlias, string framework)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentNullOrEmptyException(nameof(propertyAlias));
|
||||
|
||||
var prop = contentItem.GetProperty(propertyAlias);
|
||||
if (prop == null) throw new NullReferenceException("No property type found with alias " + propertyAlias);
|
||||
var model = prop.GetValue();
|
||||
|
||||
var asString = model as string;
|
||||
if (asString != null && string.IsNullOrEmpty(asString)) return new MvcHtmlString(string.Empty);
|
||||
|
||||
var htmlHelper = CreateHtmlHelper(model);
|
||||
return htmlHelper.GetGridHtml(contentItem, propertyAlias, framework);
|
||||
}
|
||||
|
||||
[Obsolete("This shouldn't need to be used but because the obsolete extension methods above don't have access to the current HtmlHelper, we need to create a fake one, unfortunately however this will not pertain the current views viewdata, tempdata or model state so should not be used")]
|
||||
private static HtmlHelper CreateHtmlHelper(object model)
|
||||
{
|
||||
var cc = new ControllerContext
|
||||
{
|
||||
RequestContext = UmbracoContext.Current.HttpContext.Request.RequestContext
|
||||
};
|
||||
var viewContext = new ViewContext(cc, new FakeView(), new ViewDataDictionary(model), new TempDataDictionary(), new StringWriter());
|
||||
var htmlHelper = new HtmlHelper(viewContext, new ViewPage());
|
||||
return htmlHelper;
|
||||
}
|
||||
|
||||
private class FakeView : IView
|
||||
{
|
||||
public void Render(ViewContext viewContext, TextWriter writer)
|
||||
{
|
||||
}
|
||||
return html.GetGridHtml(property, framework);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user