Merge remote-tracking branch 'origin/v8/dev' into netcore/dev

This commit is contained in:
Bjarke Berg
2020-06-11 15:16:39 +02:00
7 changed files with 455 additions and 28 deletions

View File

@@ -438,14 +438,11 @@
Write-Host "Prepare C# Documentation"
$src = "$($this.SolutionRoot)\src"
$tmp = $this.BuildTemp
$out = $this.BuildOutput
$tmp = $this.BuildTemp
$out = $this.BuildOutput
$DocFxJson = Join-Path -Path $src "\ApiDocs\docfx.json"
$DocFxSiteOutput = Join-Path -Path $tmp "\_site\*.*"
#restore nuget packages
$this.RestoreNuGet()
# run DocFx
$DocFx = $this.BuildEnv.DocFx
@@ -463,13 +460,18 @@
$src = "$($this.SolutionRoot)\src"
$out = $this.BuildOutput
# Check if the solution has been built
if (!(Test-Path "$src\Umbraco.Web.UI.Client\node_modules")) {throw "Umbraco needs to be built before generating the Angular Docs"}
"Moving to Umbraco.Web.UI.Docs folder"
cd ..\src\Umbraco.Web.UI.Docs
cd $src\Umbraco.Web.UI.Docs
"Generating the docs and waiting before executing the next commands"
& npm install
& npx gulp docs
Pop-Location
# change baseUrl
$BaseUrl = "https://our.umbraco.com/apidocs/v8/ui/"
$IndexPath = "./api/index.html"

View File

@@ -29,5 +29,7 @@ namespace Umbraco.Web.Models
public string EventElement { get; set; }
[DataMember(Name = "customProperties")]
public JObject CustomProperties { get; set; }
[DataMember(Name = "skipStepIfVisible")]
public string SkipStepIfVisible { get; set; }
}
}
}

View File

@@ -0,0 +1,405 @@
using Newtonsoft.Json;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Web.Compose;
namespace Umbraco.Tests.PropertyEditors
{
[TestFixture]
public class NestedContentPropertyComponentTests
{
[Test]
public void Invalid_Json()
{
var component = new NestedContentPropertyComponent();
Assert.DoesNotThrow(() => component.CreateNestedContentKeys("this is not json", true));
}
[Test]
public void No_Nesting()
{
var guids = new[] { Guid.NewGuid(), Guid.NewGuid() };
var guidCounter = 0;
Func<Guid> guidFactory = () => guids[guidCounter++];
var json = @"[
{""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1"",""ncContentTypeAlias"":""nested"",""text"":""woot""},
{""key"":""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",""name"":""Item 2"",""ncContentTypeAlias"":""nested"",""text"":""zoot""}
]";
var expected = json
.Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString())
.Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString());
var component = new NestedContentPropertyComponent();
var result = component.CreateNestedContentKeys(json, false, guidFactory);
Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString());
}
[Test]
public void One_Level_Nesting_Unescaped()
{
var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
var guidCounter = 0;
Func<Guid> guidFactory = () => guids[guidCounter++];
var json = @"[{
""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",
""name"": ""Item 2"",
""ncContentTypeAlias"": ""list"",
""text"": ""zoot"",
""subItems"": [{
""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"",
""name"": ""Item 2"",
""ncContentTypeAlias"": ""text"",
""text"": ""zoot""
}
]
}
]";
var expected = json
.Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString())
.Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString())
.Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString())
.Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString());
var component = new NestedContentPropertyComponent();
var result = component.CreateNestedContentKeys(json, false, guidFactory);
Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString());
}
[Test]
public void One_Level_Nesting_Escaped()
{
var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
var guidCounter = 0;
Func<Guid> guidFactory = () => guids[guidCounter++];
// we need to ensure the escaped json is consistent with how it will be re-escaped after parsing
// and this is how to do that, the result will also include quotes around it.
var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{
""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"",
""name"": ""Item 2"",
""ncContentTypeAlias"": ""text"",
""text"": ""zoot""
}
]").ToString());
var json = @"[{
""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",
""name"": ""Item 2"",
""ncContentTypeAlias"": ""list"",
""text"": ""zoot"",
""subItems"":" + subJsonEscaped + @"
}
]";
var expected = json
.Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString())
.Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString())
.Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString())
.Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString());
var component = new NestedContentPropertyComponent();
var result = component.CreateNestedContentKeys(json, false, guidFactory);
Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString());
}
[Test]
public void Nested_In_Complex_Editor_Escaped()
{
var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
var guidCounter = 0;
Func<Guid> guidFactory = () => guids[guidCounter++];
// we need to ensure the escaped json is consistent with how it will be re-escaped after parsing
// and this is how to do that, the result will also include quotes around it.
var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{
""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"",
""name"": ""Item 2"",
""ncContentTypeAlias"": ""text"",
""text"": ""zoot""
}
]").ToString());
// Complex editor such as the grid
var complexEditorJsonEscaped = @"{
""name"": ""1 column layout"",
""sections"": [
{
""grid"": ""12"",
""rows"": [
{
""name"": ""Article"",
""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"",
""areas"": [
{
""grid"": ""4"",
""controls"": [
{
""value"": ""I am quote"",
""editor"": {
""alias"": ""quote"",
""view"": ""textstring""
},
""styles"": null,
""config"": null
}],
""styles"": null,
""config"": null
},
{
""grid"": ""8"",
""controls"": [
{
""value"": ""Header"",
""editor"": {
""alias"": ""headline"",
""view"": ""textstring""
},
""styles"": null,
""config"": null
},
{
""value"": " + subJsonEscaped + @",
""editor"": {
""alias"": ""madeUpNestedContent"",
""view"": ""madeUpNestedContentInGrid""
},
""styles"": null,
""config"": null
}],
""styles"": null,
""config"": null
}],
""styles"": null,
""config"": null
}]
}]
}";
var json = @"[{
""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",
""name"": ""Item 2"",
""ncContentTypeAlias"": ""list"",
""text"": ""zoot"",
""subItems"":" + complexEditorJsonEscaped + @"
}
]";
var expected = json
.Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString())
.Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString())
.Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString())
.Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString());
var component = new NestedContentPropertyComponent();
var result = component.CreateNestedContentKeys(json, false, guidFactory);
Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString());
}
[Test]
public void No_Nesting_Generates_Keys_For_Missing_Items()
{
var guids = new[] { Guid.NewGuid() };
var guidCounter = 0;
Func<Guid> guidFactory = () => guids[guidCounter++];
var json = @"[
{""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1 my key wont change"",""ncContentTypeAlias"":""nested"",""text"":""woot""},
{""name"":""Item 2 was copied and has no key prop"",""ncContentTypeAlias"":""nested"",""text"":""zoot""}
]";
var component = new NestedContentPropertyComponent();
var result = component.CreateNestedContentKeys(json, true, guidFactory);
// Ensure the new GUID is put in a key into the JSON
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString()));
// Ensure that the original key is NOT changed/modified & still exists
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains("04a6dba8-813c-4144-8aca-86a3f24ebf08"));
}
[Test]
public void One_Level_Nesting_Escaped_Generates_Keys_For_Missing_Items()
{
var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
var guidCounter = 0;
Func<Guid> guidFactory = () => guids[guidCounter++];
// we need to ensure the escaped json is consistent with how it will be re-escaped after parsing
// and this is how to do that, the result will also include quotes around it.
var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""name"": ""Nested Item 2 was copied and has no key"",
""ncContentTypeAlias"": ""text"",
""text"": ""zoot""
}
]").ToString());
var json = @"[{
""name"": ""Item 1 was copied and has no key"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",
""name"": ""Item 2"",
""ncContentTypeAlias"": ""list"",
""text"": ""zoot"",
""subItems"":" + subJsonEscaped + @"
}
]";
var component = new NestedContentPropertyComponent();
var result = component.CreateNestedContentKeys(json, true, guidFactory);
// Ensure the new GUID is put in a key into the JSON for each item
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString()));
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString()));
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[2].ToString()));
}
[Test]
public void Nested_In_Complex_Editor_Escaped_Generates_Keys_For_Missing_Items()
{
var guids = new[] { Guid.NewGuid(), Guid.NewGuid() };
var guidCounter = 0;
Func<Guid> guidFactory = () => guids[guidCounter++];
// we need to ensure the escaped json is consistent with how it will be re-escaped after parsing
// and this is how to do that, the result will also include quotes around it.
var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{
""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""name"": ""Nested Item 2 was copied and has no key"",
""ncContentTypeAlias"": ""text"",
""text"": ""zoot""
}
]").ToString());
// Complex editor such as the grid
var complexEditorJsonEscaped = @"{
""name"": ""1 column layout"",
""sections"": [
{
""grid"": ""12"",
""rows"": [
{
""name"": ""Article"",
""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"",
""areas"": [
{
""grid"": ""4"",
""controls"": [
{
""value"": ""I am quote"",
""editor"": {
""alias"": ""quote"",
""view"": ""textstring""
},
""styles"": null,
""config"": null
}],
""styles"": null,
""config"": null
},
{
""grid"": ""8"",
""controls"": [
{
""value"": ""Header"",
""editor"": {
""alias"": ""headline"",
""view"": ""textstring""
},
""styles"": null,
""config"": null
},
{
""value"": " + subJsonEscaped + @",
""editor"": {
""alias"": ""madeUpNestedContent"",
""view"": ""madeUpNestedContentInGrid""
},
""styles"": null,
""config"": null
}],
""styles"": null,
""config"": null
}],
""styles"": null,
""config"": null
}]
}]
}";
var json = @"[{
""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""name"": ""Item 2 was copied and has no key"",
""ncContentTypeAlias"": ""list"",
""text"": ""zoot"",
""subItems"":" + complexEditorJsonEscaped + @"
}
]";
var component = new NestedContentPropertyComponent();
var result = component.CreateNestedContentKeys(json, true, guidFactory);
// Ensure the new GUID is put in a key into the JSON for each item
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString()));
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString()));
}
}
}

View File

@@ -157,6 +157,7 @@
<Compile Include="Persistence\Repositories\UserRepositoryTest.cs" />
<Compile Include="UmbracoExamine\ExamineExtensions.cs" />
<Compile Include="PropertyEditors\DataValueReferenceFactoryCollectionTests.cs" />
<Compile Include="PropertyEditors\NestedContentPropertyComponentTests.cs" />
<Compile Include="PublishedContent\NuCacheChildrenTests.cs" />
<Compile Include="PublishedContent\PublishedContentLanguageVariantTests.cs" />
<Compile Include="PublishedContent\PublishedContentSnapshotTestBase.cs" />

View File

@@ -5,14 +5,14 @@
@scope
@description
<b>Added in Umbraco 7.8</b>. The tour component is a global component and is already added to the umbraco markup.
In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco.
You can easily add you own tours to the Help-drawer or show and start tours from
<b>Added in Umbraco 7.8</b>. The tour component is a global component and is already added to the umbraco markup.
In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco.
You can easily add you own tours to the Help-drawer or show and start tours from
anywhere in the Umbraco backoffice. To see a real world example of a custom tour implementation, install <a href="https://our.umbraco.com/projects/starter-kits/the-starter-kit/">The Starter Kit</a> in Umbraco 7.8
<h1><b>Extending the help drawer with custom tours</b></h1>
The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file.
Place the file in <i>App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json</i> and it will automatically be
The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file.
Place the file in <i>App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json</i> and it will automatically be
picked up by Umbraco and shown in the Help-drawer.
<h3><b>The tour object</b></h3>
@@ -26,7 +26,7 @@ The tour object consist of two parts - The overall tour configuration and a list
"groupOrder": 200 // Control the order of tour groups
"allowDisable": // Adds a "Don't" show this tour again"-button to the intro step
"culture" : // From v7.11+. Specifies the culture of the tour (eg. en-US), if set the tour will only be shown to users with this culture set on their profile. If omitted or left empty the tour will be visible to all users
"requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load.
"requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load.
"steps": [] // tour steps - see next example
}
</pre>
@@ -43,11 +43,12 @@ The tour object consist of two parts - The overall tour configuration and a list
"backdropOpacity": 0.4 // the backdrop opacity
"view": "" // add a custom view
"customProperties" : {} // add any custom properties needed for the custom view
"skipStepIfVisible": ".dashboard div [data-element='my-tour-button']" // if we can find this DOM element on the page then we will skip this step
}
</pre>
<h1><b>Adding tours to other parts of the Umbraco backoffice</b></h1>
It is also possible to add a list of custom tours to other parts of the Umbraco backoffice,
It is also possible to add a list of custom tours to other parts of the Umbraco backoffice,
as an example on a Dashboard in a Custom section. You can then use the {@link umbraco.services.tourService tourService} to start and stop tours but you don't have to register them as part of the tour service.
<h1><b>Using the tour service</b></h1>
@@ -86,7 +87,8 @@ as an example on a Dashboard in a Custom section. You can then use the {@link um
"element": "[data-element='my-tour-button']",
"title": "Click the button",
"content": "Click the button",
"event": "click"
"event": "click",
"skipStepIfVisible": "[data-element='my-other-tour-button']"
}
]
};
@@ -257,9 +259,26 @@ In the following example you see how to run some custom logic before a step goes
// make sure we don't go too far
if (scope.model.currentStepIndex !== scope.model.steps.length) {
var upcomingStep = scope.model.steps[scope.model.currentStepIndex];
// If the currentStep JSON object has 'skipStepIfVisible'
// It's a DOM selector - if we find it then we ship over this step
if(upcomingStep.skipStepIfVisible) {
let tryFindDomEl = document.querySelector(upcomingStep.element);
if(tryFindDomEl) {
// check if element is visible:
if( tryFindDomEl.offsetWidth || tryFindDomEl.offsetHeight || tryFindDomEl.getClientRects().length ) {
// if it was visible then we skip the step.
nextStep();
}
}
}
startStep();
// tour completed - final step
} else {
// tour completed - final step
scope.loadingStep = true;
waitForPendingRerequests().then(function () {

View File

@@ -48,6 +48,9 @@ label.umb-form-check--checkbox{
&:checked ~ .umb-form-check__state .umb-form-check__check {
border-color: @ui-option-type;
}
&[type='checkbox']:checked ~ .umb-form-check__state .umb-form-check__check {
background-color: @ui-option-type;
}
&:checked:hover ~ .umb-form-check__state .umb-form-check__check {
&::before {
background: @ui-option-type-hover;

View File

@@ -188,27 +188,21 @@
"event": "click"
},
{
"element": "[data-element~='editor-data-type-picker']",
"element": "[ng-controller*='Umbraco.Editors.DataTypePickerController'] [data-element='editor-data-type-picker']",
"elementPreventClick": true,
"title": "Editor picker",
"content": "<p>In the editor picker dialog we can pick one of the many built-in editors.</p><p><em>You can choose from preconfigured data types (Reuse) or create a new configuration (Available editors)</em>.</p>"
"content": "<p>In the editor picker dialog we can pick one of the many built-in editors.</p>"
},
{
"element": "[data-element~='editor-data-type-picker'] [data-element='editor-Textarea']",
"element": "[data-element~='editor-data-type-picker'] [data-element='datatype-Textarea']",
"title": "Select editor",
"content": "Select the <b>Textarea</b> editor. This will add a textarea to the Welcome Text property.",
"event": "click"
},
{
"element": "[data-element~='editor-data-type-settings']",
"elementPreventClick": true,
"element": "[data-element='editor-data-type-picker'] [data-element='datatypeconfig-Textarea'] > a",
"title": "Editor settings",
"content": "Each property editor can have individual settings. For the textarea editor you can set a character limit but in this case it is not needed."
},
{
"element": "[data-element~='editor-data-type-settings'] [data-element='button-submit']",
"title": "Save editor",
"content": "Click <b>Submit</b> to save the changes.",
"content": "Each property editor can have individual settings. For the textarea editor you can set a character limit but in this case it is not needed.",
"event": "click"
},
{
@@ -317,7 +311,8 @@
"content": "<p>To see all our templates click the <b>small triangle</b> to the left of the templates node.</p>",
"event": "click",
"eventElement": "#tree [data-element='tree-item-templates'] [data-element='tree-item-expand']",
"view": "templatetree"
"view": "templatetree",
"skipStepIfVisible": "#tree [data-element='tree-item-templates'] > div > button[data-element=tree-item-expand].icon-navigation-down"
},
{
"element": "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page']",