Merge remote-tracking branch 'origin/v8/dev' into netcore/dev
# Conflicts: # src/Umbraco.Abstractions/Constants-AppSettings.cs # src/Umbraco.Abstractions/ContentVariationExtensions.cs # src/Umbraco.Abstractions/IMainDom.cs # src/Umbraco.Abstractions/PropertyEditors/MultiUrlPickerConfiguration.cs # src/Umbraco.Abstractions/Runtime/IMainDom.cs # src/Umbraco.Abstractions/Runtime/MainDom.cs # src/Umbraco.Core/IMainDom.cs # src/Umbraco.Core/MainDom.cs # src/Umbraco.Core/Umbraco.Core.csproj # src/Umbraco.Infrastructure/MainDom.cs # src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs # src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs # src/Umbraco.Web.UI/Umbraco/config/lang/en.xml # src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs # src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs # src/Umbraco.Web/UmbracoApplication.cs # src/Umbraco.Web/UmbracoComponentRenderer.cs
This commit is contained in:
@@ -60,10 +60,10 @@ Great question! The short version goes like this:
|
||||
|
||||

|
||||
|
||||
* **Switch to the correct branch** - switch to the v8-dev branch
|
||||
* **Switch to the correct branch** - switch to the `v8/contrib` branch
|
||||
* **Build** - build your fork of Umbraco locally as described in [building Umbraco from source code](BUILD.md)
|
||||
* **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](#questions)
|
||||
* **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v8/dev`, create a new branch first.
|
||||
* **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v8/contrib`, create a new branch first.
|
||||
* **Push** - great, now you can push the changes up to your fork on GitHub
|
||||
* **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress - you can now make use of GitHub's draft pull request status, detailed [here] (https://github.blog/2019-02-14-introducing-draft-pull-requests/)). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go.
|
||||
|
||||
@@ -158,7 +158,7 @@ To find the general areas for something you're looking to fix or improve, have a
|
||||
|
||||
### Which branch should I target for my contributions?
|
||||
|
||||
We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v8/dev`. If you are working on v8, this is the branch you should be targetting. For v7 contributions, please target 'v7/dev'.
|
||||
We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v8/contrib`. If you are working on v8, this is the branch you should be targetting. For v7 contributions, please target 'v7/dev'.
|
||||
|
||||
Please note: we are no longer accepting features for v7 but will continue to merge bug fixes as and when they arise.
|
||||
|
||||
@@ -184,10 +184,10 @@ Then when you want to get the changes from the main repository:
|
||||
|
||||
```
|
||||
git fetch upstream
|
||||
git rebase upstream/v8/dev
|
||||
git rebase upstream/v8/contrib
|
||||
```
|
||||
|
||||
In this command we're syncing with the `v8/dev` branch, but you can of course choose another one if needed.
|
||||
In this command we're syncing with the `v8/contrib` branch, but you can of course choose another one if needed.
|
||||
|
||||
(More info on how this works: [http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated](http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated))
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# [Umbraco CMS](https://umbraco.com) · [](../LICENSE.md) [](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [](CONTRIBUTING.md) [](https://twitter.com/intent/follow?screen_name=umbraco)
|
||||
# [Umbraco CMS](https://umbraco.com) · [](../LICENSE.md) [](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [](CONTRIBUTING.md) [](https://twitter.com/intent/follow?screen_name=umbraco)
|
||||
|
||||
Umbraco is the friendliest, most flexible and fastest growing ASP.NET CMS, and used by more than 500,000 websites worldwide. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social.
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 24 KiB |
@@ -9,6 +9,12 @@ namespace Umbraco.Core
|
||||
/// </summary>
|
||||
public static class AppSettings
|
||||
{
|
||||
public const string MainDomLock = "Umbraco.Core.MainDom.Lock";
|
||||
|
||||
// TODO: Kill me - still used in Umbraco.Core.IO.SystemFiles:27
|
||||
[Obsolete("We need to kill this appsetting as we do not use XML content cache umbraco.config anymore due to NuCache")]
|
||||
public const string ContentXML = "Umbraco.Core.ContentXML"; //umbracoContentXML
|
||||
|
||||
/// <summary>
|
||||
/// TODO: FILL ME IN
|
||||
/// </summary>
|
||||
|
||||
@@ -12,126 +12,260 @@ namespace Umbraco.Core
|
||||
/// <summary>
|
||||
/// Determines whether the content type is invariant.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the content type is invariant.
|
||||
/// </returns>
|
||||
public static bool VariesByNothing(this ISimpleContentType contentType) => contentType.Variations.VariesByNothing();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by culture.
|
||||
/// </summary>
|
||||
public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by segment.
|
||||
/// </summary>
|
||||
public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type is invariant.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the content type is invariant.
|
||||
/// </returns>
|
||||
public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by culture.
|
||||
/// </summary>
|
||||
/// <remarks>And then it could also vary by segment.</remarks>
|
||||
public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by segment.
|
||||
/// </summary>
|
||||
/// <remarks>And then it could also vary by culture.</remarks>
|
||||
public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by culture and segment.
|
||||
/// </summary>
|
||||
public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => contentType.Variations.VariesByCultureAndSegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the property type is invariant.
|
||||
/// </summary>
|
||||
public static bool VariesByNothing(this IPropertyType propertyType) => propertyType.Variations.VariesByNothing();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the property type varies by culture.
|
||||
/// </summary>
|
||||
/// <remarks>And then it could also vary by segment.</remarks>
|
||||
public static bool VariesByCulture(this IPropertyType propertyType) => propertyType.Variations.VariesByCulture();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the property type varies by segment.
|
||||
/// </summary>
|
||||
/// <remarks>And then it could also vary by culture.</remarks>
|
||||
public static bool VariesBySegment(this IPropertyType propertyType) => propertyType.Variations.VariesBySegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the property type varies by culture and segment.
|
||||
/// </summary>
|
||||
public static bool VariesByCultureAndSegment(this IPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type is invariant.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the content type is invariant.
|
||||
/// </returns>
|
||||
public static bool VariesByNothing(this IPublishedContentType contentType) => contentType.Variations.VariesByNothing();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by culture.
|
||||
/// Determines whether the property type is invariant.
|
||||
/// </summary>
|
||||
/// <remarks>And then it could also vary by segment.</remarks>
|
||||
public static bool VariesByCulture(this IPublishedContentType contentType) => contentType.Variations.VariesByCulture();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by segment.
|
||||
/// </summary>
|
||||
/// <remarks>And then it could also vary by culture.</remarks>
|
||||
public static bool VariesBySegment(this IPublishedContentType contentType) => contentType.Variations.VariesBySegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by culture and segment.
|
||||
/// </summary>
|
||||
public static bool VariesByCultureAndSegment(this IPublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment();
|
||||
/// <param name="propertyType">The property type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the property type is invariant.
|
||||
/// </returns>
|
||||
public static bool VariesByNothing(this IPropertyType propertyType) => propertyType.Variations.VariesByNothing();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the property type is invariant.
|
||||
/// </summary>
|
||||
/// <param name="propertyType">The property type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the property type is invariant.
|
||||
/// </returns>
|
||||
public static bool VariesByNothing(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByNothing();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the property type varies by culture.
|
||||
/// </summary>
|
||||
public static bool VariesByCulture(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the property type varies by segment.
|
||||
/// </summary>
|
||||
public static bool VariesBySegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the property type varies by culture and segment.
|
||||
/// </summary>
|
||||
public static bool VariesByCultureAndSegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a variation is invariant.
|
||||
/// </summary>
|
||||
/// <param name="variation">The variation.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the variation is invariant.
|
||||
/// </returns>
|
||||
public static bool VariesByNothing(this ContentVariation variation) => variation == ContentVariation.Nothing;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by culture.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the content type varies by culture.
|
||||
/// </returns>
|
||||
public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by culture.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the content type varies by culture.
|
||||
/// </returns>
|
||||
public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by culture.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the content type varies by culture.
|
||||
/// </returns>
|
||||
public static bool VariesByCulture(this IPublishedContentType contentType) => contentType.Variations.VariesByCulture();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the property type varies by culture.
|
||||
/// </summary>
|
||||
/// <param name="propertyType">The property type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the property type varies by culture.
|
||||
/// </returns>
|
||||
public static bool VariesByCulture(this IPropertyType propertyType) => propertyType.Variations.VariesByCulture();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the property type varies by culture.
|
||||
/// </summary>
|
||||
/// <param name="propertyType">The property type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the property type varies by culture.
|
||||
/// </returns>
|
||||
public static bool VariesByCulture(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a variation varies by culture.
|
||||
/// </summary>
|
||||
/// <remarks>And then it could also vary by segment.</remarks>
|
||||
/// <param name="variation">The variation.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the variation varies by culture.
|
||||
/// </returns>
|
||||
public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by segment.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the content type varies by segment.
|
||||
/// </returns>
|
||||
public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by segment.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the content type varies by segment.
|
||||
/// </returns>
|
||||
public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by segment.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the content type varies by segment.
|
||||
/// </returns>
|
||||
public static bool VariesBySegment(this IPublishedContentType contentType) => contentType.Variations.VariesBySegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the property type varies by segment.
|
||||
/// </summary>
|
||||
/// <param name="propertyType">The property type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the property type varies by segment.
|
||||
/// </returns>
|
||||
public static bool VariesBySegment(this IPropertyType propertyType) => propertyType.Variations.VariesBySegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the property type varies by segment.
|
||||
/// </summary>
|
||||
/// <param name="propertyType">The property type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the property type varies by segment.
|
||||
/// </returns>
|
||||
public static bool VariesBySegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a variation varies by segment.
|
||||
/// </summary>
|
||||
/// <remarks>And then it could also vary by culture.</remarks>
|
||||
/// <param name="variation">The variation.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the variation varies by segment.
|
||||
/// </returns>
|
||||
public static bool VariesBySegment(this ContentVariation variation) => (variation & ContentVariation.Segment) > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by culture and segment.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the content type varies by culture and segment.
|
||||
/// </returns>
|
||||
public static bool VariesByCultureAndSegment(this ISimpleContentType contentType) => contentType.Variations.VariesByCultureAndSegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by culture and segment.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the content type varies by culture and segment.
|
||||
/// </returns>
|
||||
public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => contentType.Variations.VariesByCultureAndSegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content type varies by culture and segment.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the content type varies by culture and segment.
|
||||
/// </returns>
|
||||
public static bool VariesByCultureAndSegment(this IPublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the property type varies by culture and segment.
|
||||
/// </summary>
|
||||
/// <param name="propertyType">The property type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the property type varies by culture and segment.
|
||||
/// </returns>
|
||||
public static bool VariesByCultureAndSegment(this IPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the property type varies by culture and segment.
|
||||
/// </summary>
|
||||
/// <param name="propertyType">The property type.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the property type varies by culture and segment.
|
||||
/// </returns>
|
||||
public static bool VariesByCultureAndSegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a variation varies by culture and segment.
|
||||
/// </summary>
|
||||
/// <param name="variation">The variation.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the variation varies by culture and segment.
|
||||
/// </returns>
|
||||
public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) == ContentVariation.CultureAndSegment;
|
||||
|
||||
/// <summary>
|
||||
/// Sets or removes the content type variation depending on the specified value.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <param name="variation">The variation to set or remove.</param>
|
||||
/// <param name="value">If set to <c>true</c> sets the variation; otherwise, removes the variation.</param>
|
||||
/// <remarks>
|
||||
/// This method does not support setting the variation to nothing.
|
||||
/// </remarks>
|
||||
public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => contentType.Variations = contentType.Variations.SetFlag(variation, value);
|
||||
|
||||
/// <summary>
|
||||
/// Sets or removes the property type variation depending on the specified value.
|
||||
/// </summary>
|
||||
/// <param name="propertyType">The property type.</param>
|
||||
/// <param name="variation">The variation to set or remove.</param>
|
||||
/// <param name="value">If set to <c>true</c> sets the variation; otherwise, removes the variation.</param>
|
||||
/// <remarks>
|
||||
/// This method does not support setting the variation to nothing.
|
||||
/// </remarks>
|
||||
public static void SetVariesBy(this IPropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetFlag(variation, value);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the variations with the variation set or removed depending on the specified value.
|
||||
/// </summary>
|
||||
/// <param name="variations">The existing variations.</param>
|
||||
/// <param name="variation">The variation to set or remove.</param>
|
||||
/// <param name="value">If set to <c>true</c> sets the variation; otherwise, removes the variation.</param>
|
||||
/// <returns>
|
||||
/// The variations with the variation set or removed.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This method does not support setting the variation to nothing.
|
||||
/// </remarks>
|
||||
public static ContentVariation SetFlag(this ContentVariation variations, ContentVariation variation, bool value = true)
|
||||
{
|
||||
return value
|
||||
? variations | variation // Set flag using bitwise logical OR
|
||||
: variations & ~variation; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a combination of culture and segment is valid for the variation.
|
||||
/// </summary>
|
||||
@@ -140,16 +274,18 @@ namespace Umbraco.Core
|
||||
/// <param name="segment">The segment.</param>
|
||||
/// <param name="exact">A value indicating whether to perform exact validation.</param>
|
||||
/// <param name="wildcards">A value indicating whether to support wildcards.</param>
|
||||
/// <param name="throwIfInvalid">A value indicating whether to throw a <see cref="NotSupportedException"/> when the combination is invalid.</param>
|
||||
/// <returns>True if the combination is valid; otherwise false.</returns>
|
||||
/// <param name="throwIfInvalid">A value indicating whether to throw a <see cref="NotSupportedException" /> when the combination is invalid.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the combination is valid; otherwise <c>false</c>.
|
||||
/// </returns>
|
||||
/// <exception cref="NotSupportedException">Occurs when the combination is invalid, and <paramref name="throwIfInvalid" /> is true.</exception>
|
||||
/// <remarks>
|
||||
/// <para>When validation is exact, the combination must match the variation exactly. For instance, if the variation is Culture, then
|
||||
/// a culture is required. When validation is not strict, the combination must be equivalent, or more restrictive: if the variation is
|
||||
/// Culture, an invariant combination is ok.</para>
|
||||
/// <para>Basically, exact is for one content type, or one property type, and !exact is for "all property types" of one content type.</para>
|
||||
/// <para>Both <paramref name="culture"/> and <paramref name="segment"/> can be "*" to indicate "all of them".</para>
|
||||
/// <para>Both <paramref name="culture" /> and <paramref name="segment" /> can be "*" to indicate "all of them".</para>
|
||||
/// </remarks>
|
||||
/// <exception cref="NotSupportedException">Occurs when the combination is invalid, and <paramref name="throwIfInvalid"/> is true.</exception>
|
||||
public static bool ValidateVariation(this ContentVariation variation, string culture, string segment, bool exact, bool wildcards, bool throwIfInvalid)
|
||||
{
|
||||
culture = culture.NullOrWhiteSpaceAsNull();
|
||||
@@ -171,6 +307,7 @@ namespace Umbraco.Core
|
||||
{
|
||||
if (throwIfInvalid)
|
||||
throw new NotSupportedException($"Culture may not be null because culture variation is enabled.");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -183,6 +320,7 @@ namespace Umbraco.Core
|
||||
{
|
||||
if (throwIfInvalid)
|
||||
throw new NotSupportedException($"Culture \"{culture}\" is invalid because culture variation is disabled.");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -195,6 +333,7 @@ namespace Umbraco.Core
|
||||
{
|
||||
if (throwIfInvalid)
|
||||
throw new NotSupportedException($"Segment \"{segment}\" is invalid because segment variation is disabled.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,81 +3,42 @@
|
||||
namespace Umbraco.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods to enums.
|
||||
/// Provides extension methods to <see cref="Enum"/>.
|
||||
/// </summary>
|
||||
public static class EnumExtensions
|
||||
{
|
||||
// note:
|
||||
// - no need to HasFlagExact, that's basically an == test
|
||||
// - HasFlagAll cannot be named HasFlag because ext. methods never take priority over instance methods
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a flag enum has all the specified values.
|
||||
/// Determines whether all the flags/bits are set within the enum value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>True when all bits set in <paramref name="uses"/> are set in <paramref name="use"/>, though other bits may be set too.</para>
|
||||
/// <para>This is the behavior of the original <see cref="Enum.HasFlag"/> method.</para>
|
||||
/// </remarks>
|
||||
public static bool HasFlagAll<T>(this T use, T uses)
|
||||
/// <typeparam name="T">The enum type.</typeparam>
|
||||
/// <param name="value">The enum value.</param>
|
||||
/// <param name="flags">The flags.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if all the flags/bits are set within the enum value; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
[Obsolete("Use Enum.HasFlag() or bitwise operations (if performance is important) instead.")]
|
||||
public static bool HasFlagAll<T>(this T value, T flags)
|
||||
where T : Enum
|
||||
{
|
||||
var num = Convert.ToUInt64(use);
|
||||
var nums = Convert.ToUInt64(uses);
|
||||
|
||||
return (num & nums) == nums;
|
||||
return value.HasFlag(flags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a flag enum has any of the specified values.
|
||||
/// Determines whether any of the flags/bits are set within the enum value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>True when at least one of the bits set in <paramref name="uses"/> is set in <paramref name="use"/>.</para>
|
||||
/// </remarks>
|
||||
public static bool HasFlagAny<T>(this T use, T uses)
|
||||
/// <typeparam name="T">The enum type.</typeparam>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="flags">The flags.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if any of the flags/bits are set within the enum value; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool HasFlagAny<T>(this T value, T flags)
|
||||
where T : Enum
|
||||
{
|
||||
var num = Convert.ToUInt64(use);
|
||||
var nums = Convert.ToUInt64(uses);
|
||||
var v = Convert.ToUInt64(value);
|
||||
var f = Convert.ToUInt64(flags);
|
||||
|
||||
return (num & nums) > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a flag of the given input enum
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="input">Enum to set flag of</param>
|
||||
/// <param name="flag">Flag to set</param>
|
||||
/// <returns>A new enum with the flag set</returns>
|
||||
public static T SetFlag<T>(this T input, T flag)
|
||||
where T : Enum
|
||||
{
|
||||
var i = Convert.ToUInt64(input);
|
||||
var f = Convert.ToUInt64(flag);
|
||||
|
||||
// bitwise OR to set flag f of enum i
|
||||
var result = i | f;
|
||||
|
||||
return (T)Enum.ToObject(typeof(T), result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsets a flag of the given input enum
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="input">Enum to unset flag of</param>
|
||||
/// <param name="flag">Flag to unset</param>
|
||||
/// <returns>A new enum with the flag unset</returns>
|
||||
public static T UnsetFlag<T>(this T input, T flag)
|
||||
where T : Enum
|
||||
{
|
||||
var i = Convert.ToUInt64(input);
|
||||
var f = Convert.ToUInt64(flag);
|
||||
|
||||
// bitwise AND combined with bitwise complement to unset flag f of enum i
|
||||
var result = i & ~f;
|
||||
|
||||
return (T)Enum.ToObject(typeof(T), result);
|
||||
return (v & f) > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Umbraco.Core.Models
|
||||
{
|
||||
internal static class MediaTypeExtensions
|
||||
{
|
||||
internal static bool IsSystemMediaType(this IMediaType mediaType) =>
|
||||
mediaType.Alias == Constants.Conventions.MediaTypes.File
|
||||
|| mediaType.Alias == Constants.Conventions.MediaTypes.Folder
|
||||
|| mediaType.Alias == Constants.Conventions.MediaTypes.Image;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,11 @@ namespace Umbraco.Core
|
||||
/// </summary>
|
||||
public static class Locks
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IMainDom"/> lock
|
||||
/// </summary>
|
||||
public const int MainDom = -1000;
|
||||
|
||||
/// <summary>
|
||||
/// All servers.
|
||||
/// </summary>
|
||||
|
||||
@@ -16,6 +16,9 @@ namespace Umbraco.Web.PropertyEditors
|
||||
Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")]
|
||||
public bool IgnoreUserStartNodes { get; set; }
|
||||
|
||||
|
||||
[ConfigurationField("hideAnchor",
|
||||
"Hide anchor/query string input", "boolean",
|
||||
Description = "Selecting this hides the anchor/query string input field in the linkpicker overlay.")]
|
||||
public bool HideAnchor { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
|
||||
// TODO: Can't change namespace due to breaking changes, change in netcore
|
||||
namespace Umbraco.Core
|
||||
{
|
||||
/// <summary>
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.Core.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// An application-wide distributed lock
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Disposing releases the lock
|
||||
/// </remarks>
|
||||
public interface IMainDomLock : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Acquires an application-wide distributed lock
|
||||
/// </summary>
|
||||
/// <param name="millisecondsTimeout"></param>
|
||||
/// <returns>
|
||||
/// An awaitable boolean value which will be false if the elapsed millsecondsTimeout value is exceeded
|
||||
/// </returns>
|
||||
Task<bool> AcquireLockAsync(int millisecondsTimeout);
|
||||
|
||||
/// <summary>
|
||||
/// Wait on a background thread to receive a signal from another AppDomain
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task ListenAsync();
|
||||
}
|
||||
}
|
||||
+52
-58
@@ -6,8 +6,9 @@ using System.Threading;
|
||||
using Umbraco.Core.Hosting;
|
||||
using Umbraco.Core.Logging;
|
||||
|
||||
namespace Umbraco.Core
|
||||
namespace Umbraco.Core.Runtime
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Provides the full implementation of <see cref="IMainDom"/>.
|
||||
/// </summary>
|
||||
@@ -21,18 +22,11 @@ namespace Umbraco.Core
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
private readonly IMainDomLock _mainDomLock;
|
||||
|
||||
// our own lock for local consistency
|
||||
private object _locko = new object();
|
||||
|
||||
// async lock representing the main domain lock
|
||||
private readonly SystemLock _systemLock;
|
||||
private IDisposable _systemLocker;
|
||||
|
||||
// event wait handle used to notify current main domain that it should
|
||||
// release the lock because a new domain wants to be the main domain
|
||||
private readonly EventWaitHandle _signal;
|
||||
|
||||
private bool _isInitialized;
|
||||
// indicates whether...
|
||||
private bool _isMainDom; // we are the main domain
|
||||
@@ -41,41 +35,20 @@ namespace Umbraco.Core
|
||||
// actions to run before releasing the main domain
|
||||
private readonly List<KeyValuePair<int, Action>> _callbacks = new List<KeyValuePair<int, Action>>();
|
||||
|
||||
private const int LockTimeoutMilliseconds = 90000; // (1.5 * 60 * 1000) == 1 min 30 seconds
|
||||
private const int LockTimeoutMilliseconds = 40000; // 40 seconds
|
||||
|
||||
#endregion
|
||||
|
||||
#region Ctor
|
||||
|
||||
// initializes a new instance of MainDom
|
||||
public MainDom(ILogger logger, IHostingEnvironment hostingEnvironment)
|
||||
public MainDom(ILogger logger, IHostingEnvironment hostingEnvironment, IMainDomLock systemLock)
|
||||
{
|
||||
|
||||
hostingEnvironment.RegisterObject(this);
|
||||
|
||||
_logger = logger;
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
|
||||
// HostingEnvironment.ApplicationID is null in unit tests, making ReplaceNonAlphanumericChars fail
|
||||
var appId = hostingEnvironment.ApplicationId?.ReplaceNonAlphanumericChars(string.Empty);
|
||||
|
||||
// combining with the physical path because if running on eg IIS Express,
|
||||
// two sites could have the same appId even though they are different.
|
||||
//
|
||||
// now what could still collide is... two sites, running in two different processes
|
||||
// and having the same appId, and running on the same app physical path
|
||||
//
|
||||
// we *cannot* use the process ID here because when an AppPool restarts it is
|
||||
// a new process for the same application path
|
||||
|
||||
var appPath = hostingEnvironment.ApplicationPhysicalPath?.ToLowerInvariant() ?? string.Empty;
|
||||
var hash = (appId + ":::" + appPath).GenerateHash<SHA1>();
|
||||
|
||||
var lockName = "UMBRACO-" + hash + "-MAINDOM-LCK";
|
||||
_systemLock = new SystemLock(lockName);
|
||||
|
||||
var eventName = "UMBRACO-" + hash + "-MAINDOM-EVT";
|
||||
_signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
|
||||
_mainDomLock = systemLock;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -144,13 +117,14 @@ namespace Umbraco.Core
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug<MainDom>("Stopped ({SignalSource})", source);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// in any case...
|
||||
_isMainDom = false;
|
||||
_systemLocker?.Dispose();
|
||||
_mainDomLock.Dispose();
|
||||
_logger.Info<MainDom>("Released ({SignalSource})", source);
|
||||
}
|
||||
|
||||
@@ -170,37 +144,34 @@ namespace Umbraco.Core
|
||||
|
||||
_logger.Info<MainDom>("Acquiring.");
|
||||
|
||||
// signal other instances that we want the lock, then wait one the lock,
|
||||
// which may timeout, and this is accepted - see comments below
|
||||
// Get the lock
|
||||
var acquired = _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).GetAwaiter().GetResult();
|
||||
|
||||
// signal, then wait for the lock, then make sure the event is
|
||||
// reset (maybe there was noone listening..)
|
||||
_signal.Set();
|
||||
if (!acquired)
|
||||
{
|
||||
_logger.Info<MainDom>("Cannot acquire (timeout).");
|
||||
|
||||
// if more than 1 instance reach that point, one will get the lock
|
||||
// and the other one will timeout, which is accepted
|
||||
// In previous versions we'd let a TimeoutException be thrown
|
||||
// and the appdomain would not start. We have the opportunity to allow it to
|
||||
// start without having MainDom? This would mean that it couldn't write
|
||||
// to nucache/examine and would only be ok if this was a super short lived appdomain.
|
||||
// maybe safer to just keep throwing in this case.
|
||||
|
||||
throw new TimeoutException("Cannot acquire MainDom");
|
||||
// return false;
|
||||
}
|
||||
|
||||
//This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset.
|
||||
try
|
||||
{
|
||||
_systemLocker = _systemLock.Lock(LockTimeoutMilliseconds);
|
||||
// Listen for the signal from another AppDomain coming online to release the lock
|
||||
_mainDomLock.ListenAsync().ContinueWith(_ => OnSignal("signal"));
|
||||
}
|
||||
finally
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
// we need to reset the event, because otherwise we would end up
|
||||
// signaling ourselves and committing suicide immediately.
|
||||
// only 1 instance can reach that point, but other instances may
|
||||
// have started and be trying to get the lock - they will timeout,
|
||||
// which is accepted
|
||||
|
||||
_signal.Reset();
|
||||
// the waiting task could be canceled if this appdomain is naturally shutting down, we'll just swallow this exception
|
||||
_logger.Warn<MainDom>(ex, ex.Message);
|
||||
}
|
||||
|
||||
//WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread
|
||||
|
||||
_signal.WaitOneAsync()
|
||||
.ContinueWith(_ => OnSignal("signal"));
|
||||
|
||||
_logger.Info<MainDom>("Acquired.");
|
||||
return true;
|
||||
}
|
||||
@@ -208,6 +179,10 @@ namespace Umbraco.Core
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the current domain is the main domain.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The lazy initializer call will only call the Acquire callback when it's not been initialized, else it will just return
|
||||
/// the value from _isMainDom which means when we set _isMainDom to false again after being signaled, this will return false;
|
||||
/// </remarks>
|
||||
public bool IsMainDom => LazyInitializer.EnsureInitialized(ref _isMainDom, ref _isInitialized, ref _locko, () => Acquire());
|
||||
|
||||
// IRegisteredObject
|
||||
@@ -233,8 +208,7 @@ namespace Umbraco.Core
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_signal?.Close();
|
||||
_signal?.Dispose();
|
||||
_mainDomLock.Dispose();
|
||||
}
|
||||
|
||||
disposedValue = true;
|
||||
@@ -247,5 +221,25 @@ namespace Umbraco.Core
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static string GetMainDomId(IHostingEnvironment hostingEnvironment)
|
||||
{
|
||||
// HostingEnvironment.ApplicationID is null in unit tests, making ReplaceNonAlphanumericChars fail
|
||||
var appId = hostingEnvironment.ApplicationId?.ReplaceNonAlphanumericChars(string.Empty) ?? string.Empty;
|
||||
|
||||
// combining with the physical path because if running on eg IIS Express,
|
||||
// two sites could have the same appId even though they are different.
|
||||
//
|
||||
// now what could still collide is... two sites, running in two different processes
|
||||
// and having the same appId, and running on the same app physical path
|
||||
//
|
||||
// we *cannot* use the process ID here because when an AppPool restarts it is
|
||||
// a new process for the same application path
|
||||
|
||||
var appPath = hostingEnvironment.ApplicationPhysicalPath?.ToLowerInvariant() ?? string.Empty;
|
||||
var hash = (appId + ":::" + appPath).GenerateHash<SHA1>();
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,11 @@ namespace Umbraco.Core.Services
|
||||
|
||||
int Count();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true or false depending on whether content nodes have been created based on the provided content type id.
|
||||
/// </summary>
|
||||
bool HasContentNodes(int id);
|
||||
|
||||
IEnumerable<TItem> GetAll(params int[] ids);
|
||||
IEnumerable<TItem> GetAll(IEnumerable<Guid> ids);
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Logging\Viewer" />
|
||||
<Folder Include="Migrations\Upgrade\V_8_6_0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -155,6 +155,8 @@ namespace Umbraco.Core.Migrations.Install
|
||||
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Domains, Name = "Domains" });
|
||||
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.KeyValues, Name = "KeyValues" });
|
||||
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Languages, Name = "Languages" });
|
||||
|
||||
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" });
|
||||
}
|
||||
|
||||
private void CreateContentTypeData()
|
||||
|
||||
@@ -186,13 +186,12 @@ namespace Umbraco.Core.Migrations.Upgrade
|
||||
To<RenameUserLoginDtoDateIndex>("{0372A42B-DECF-498D-B4D1-6379E907EB94}");
|
||||
To<FixContentNuCascade>("{5B1E0D93-F5A3-449B-84BA-65366B84E2D4}");
|
||||
|
||||
// to 8.5.0...
|
||||
// to 8.6.0...
|
||||
To<UpdateRelationTypeTable>("{4759A294-9860-46BC-99F9-B4C975CAE580}");
|
||||
To<AddNewRelationTypes>("{0BC866BC-0665-487A-9913-0290BD0169AD}");
|
||||
|
||||
// to 8.6.0
|
||||
To<AddPropertyTypeValidationMessageColumns>("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}");
|
||||
To<MissingContentVersionsIndexes>("{EE288A91-531B-4995-8179-1D62D9AA3E2E}");
|
||||
To<AddMainDomLock>("{2AB29964-02A1-474D-BD6B-72148D2A53A2}");
|
||||
|
||||
//FINAL
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using Umbraco.Core.Persistence.Dtos;
|
||||
|
||||
namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0
|
||||
{
|
||||
public class AddMainDomLock : MigrationBase
|
||||
{
|
||||
public AddMainDomLock(IMigrationContext context)
|
||||
: base(context)
|
||||
{ }
|
||||
|
||||
public override void Migrate()
|
||||
{
|
||||
Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" });
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -3,6 +3,7 @@ using Umbraco.Core.Persistence.Dtos;
|
||||
|
||||
namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0
|
||||
{
|
||||
|
||||
public class AddPropertyTypeValidationMessageColumns : MigrationBase
|
||||
{
|
||||
public AddPropertyTypeValidationMessageColumns(IMigrationContext context)
|
||||
|
||||
@@ -40,5 +40,8 @@ namespace Umbraco.Web.Models
|
||||
|
||||
[DataMember(Name = "culture")]
|
||||
public string Culture { get; set; }
|
||||
|
||||
[DataMember(Name = "contentType")]
|
||||
public string ContentType { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Umbraco.Web.Models.ContentEditing
|
||||
[DataContract(Name = "contentType", Namespace = "")]
|
||||
public class MediaTypeDisplay : ContentTypeCompositionDisplay<PropertyTypeDisplay>
|
||||
{
|
||||
|
||||
[DataMember(Name = "isSystemMediaType")]
|
||||
public bool IsSystemMediaType { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,5 +26,10 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
/// <param name="contentPath"></param>
|
||||
/// <returns></returns>
|
||||
bool HasContainerInPath(string contentPath);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true or false depending on whether content nodes have been created based on the provided content type id.
|
||||
/// </summary>
|
||||
bool HasContentNodes(int id);
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -1326,6 +1326,17 @@ WHERE {Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentT
|
||||
return Database.ExecuteScalar<int>(sql) > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true or false depending on whether content nodes have been created based on the provided content type id.
|
||||
/// </summary>
|
||||
public bool HasContentNodes(int id)
|
||||
{
|
||||
var sql = new Sql(
|
||||
$"SELECT CASE WHEN EXISTS (SELECT * FROM {Constants.DatabaseSchema.Tables.Content} WHERE contentTypeId = @id) THEN 1 ELSE 0 END",
|
||||
new { id });
|
||||
return Database.ExecuteScalar<int>(sql) == 1;
|
||||
}
|
||||
|
||||
protected override IEnumerable<string> GetDeleteClauses()
|
||||
{
|
||||
// in theory, services should have ensured that content items of the given content type
|
||||
|
||||
@@ -251,6 +251,11 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName)
|
||||
}
|
||||
|
||||
public override void WriteLock(IDatabase db, params int[] lockIds)
|
||||
{
|
||||
WriteLock(db, TimeSpan.FromMilliseconds(1800), lockIds);
|
||||
}
|
||||
|
||||
public void WriteLock(IDatabase db, TimeSpan timeout, params int[] lockIds)
|
||||
{
|
||||
// soon as we get Database, a transaction is started
|
||||
|
||||
@@ -261,7 +266,7 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName)
|
||||
// *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks
|
||||
foreach (var lockId in lockIds)
|
||||
{
|
||||
db.Execute(@"SET LOCK_TIMEOUT 1800;");
|
||||
db.Execute($"SET LOCK_TIMEOUT {timeout.TotalMilliseconds};");
|
||||
var i = db.Execute(@"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId });
|
||||
if (i == 0) // ensure we are actually locking!
|
||||
throw new ArgumentException($"LockObject with id={lockId} does not exist.");
|
||||
|
||||
@@ -167,6 +167,18 @@ namespace Umbraco.Core.Runtime
|
||||
// type finder/loader
|
||||
var typeLoader = new TypeLoader(IOHelper, TypeFinder, appCaches.RuntimeCache, new DirectoryInfo(HostingEnvironment.LocalTempPath), ProfilingLogger);
|
||||
|
||||
// runtime state
|
||||
// beware! must use '() => _factory.GetInstance<T>()' and NOT '_factory.GetInstance<T>'
|
||||
// as the second one captures the current value (null) and therefore fails
|
||||
_state = new RuntimeState(Logger,
|
||||
Configs.Settings(), Configs.Global(),
|
||||
new Lazy<IMainDom>(() => _factory.GetInstance<IMainDom>()),
|
||||
new Lazy<IServerRegistrar>(() => _factory.GetInstance<IServerRegistrar>()),
|
||||
UmbracoVersion, HostingEnvironment, BackOfficeInfo)
|
||||
{
|
||||
Level = RuntimeLevel.Boot
|
||||
};
|
||||
|
||||
// create the composition
|
||||
composition = new Composition(register, typeLoader, ProfilingLogger, _state, Configs, IOHelper, appCaches);
|
||||
composition.RegisterEssentials(Logger, Profiler, ProfilingLogger, MainDom, appCaches, databaseFactory, typeLoader, _state, TypeFinder, IOHelper, UmbracoVersion, DbProviderFactoryCreator);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core.Hosting;
|
||||
using Umbraco.Core.Logging;
|
||||
|
||||
namespace Umbraco.Core.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// Uses a system-wide Semaphore and EventWaitHandle to synchronize the current AppDomain
|
||||
/// </summary>
|
||||
public class MainDomSemaphoreLock : IMainDomLock
|
||||
{
|
||||
private readonly SystemLock _systemLock;
|
||||
|
||||
// event wait handle used to notify current main domain that it should
|
||||
// release the lock because a new domain wants to be the main domain
|
||||
private readonly EventWaitHandle _signal;
|
||||
private readonly ILogger _logger;
|
||||
private IDisposable _lockRelease;
|
||||
|
||||
public MainDomSemaphoreLock(ILogger logger, IHostingEnvironment hostingEnvironment)
|
||||
{
|
||||
var lockName = "UMBRACO-" + MainDom.GetMainDomId(hostingEnvironment) + "-MAINDOM-LCK";
|
||||
_systemLock = new SystemLock(lockName);
|
||||
|
||||
var eventName = "UMBRACO-" + MainDom.GetMainDomId(hostingEnvironment) + "-MAINDOM-EVT";
|
||||
_signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
//WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread
|
||||
public Task ListenAsync() => _signal.WaitOneAsync();
|
||||
|
||||
public Task<bool> AcquireLockAsync(int millisecondsTimeout)
|
||||
{
|
||||
// signal other instances that we want the lock, then wait on the lock,
|
||||
// which may timeout, and this is accepted - see comments below
|
||||
|
||||
// signal, then wait for the lock, then make sure the event is
|
||||
// reset (maybe there was noone listening..)
|
||||
_signal.Set();
|
||||
|
||||
// if more than 1 instance reach that point, one will get the lock
|
||||
// and the other one will timeout, which is accepted
|
||||
|
||||
//This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset.
|
||||
try
|
||||
{
|
||||
_lockRelease = _systemLock.Lock(millisecondsTimeout);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
catch (TimeoutException ex)
|
||||
{
|
||||
_logger.Error<MainDomSemaphoreLock>(ex);
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// we need to reset the event, because otherwise we would end up
|
||||
// signaling ourselves and committing suicide immediately.
|
||||
// only 1 instance can reach that point, but other instances may
|
||||
// have started and be trying to get the lock - they will timeout,
|
||||
// which is accepted
|
||||
|
||||
_signal.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
#region IDisposable Support
|
||||
private bool disposedValue = false; // To detect redundant calls
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_lockRelease?.Dispose();
|
||||
_signal.Close();
|
||||
_signal.Dispose();
|
||||
}
|
||||
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
// This code added to correctly implement the disposable pattern.
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
|
||||
Dispose(true);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Persistence;
|
||||
using Umbraco.Core.Persistence.Dtos;
|
||||
using Umbraco.Core.Persistence.Mappers;
|
||||
using Umbraco.Core.Persistence.SqlSyntax;
|
||||
|
||||
namespace Umbraco.Core.Runtime
|
||||
{
|
||||
public class SqlMainDomLock : IMainDomLock
|
||||
{
|
||||
private string _lockId;
|
||||
private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom";
|
||||
private const string UpdatedSuffix = "_updated";
|
||||
private readonly ILogger _logger;
|
||||
private IUmbracoDatabase _db;
|
||||
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider();
|
||||
private bool _mainDomChanging = false;
|
||||
private readonly UmbracoDatabaseFactory _dbFactory;
|
||||
private bool _hasError;
|
||||
private object _locker = new object();
|
||||
|
||||
public SqlMainDomLock(ILogger logger, Configs configs, IDbProviderFactoryCreator dbProviderFactoryCreator)
|
||||
{
|
||||
// unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer
|
||||
_lockId = Guid.NewGuid().ToString();
|
||||
_logger = logger;
|
||||
_dbFactory = new UmbracoDatabaseFactory(
|
||||
Constants.System.UmbracoConnectionName,
|
||||
_logger,
|
||||
new Lazy<IMapperCollection>(() => new MapperCollection(Enumerable.Empty<BaseMapper>())),
|
||||
configs, dbProviderFactoryCreator
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<bool> AcquireLockAsync(int millisecondsTimeout)
|
||||
{
|
||||
if (!(_dbFactory.SqlContext.SqlSyntax is SqlServerSyntaxProvider sqlServerSyntaxProvider))
|
||||
throw new NotSupportedException("SqlMainDomLock is only supported for Sql Server");
|
||||
|
||||
_sqlServerSyntax = sqlServerSyntaxProvider;
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Acquiring lock...");
|
||||
|
||||
var db = GetDatabase();
|
||||
|
||||
var tempId = Guid.NewGuid().ToString();
|
||||
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
// wait to get a write lock
|
||||
_sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (IsLockTimeoutException(ex))
|
||||
{
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, could not acquire MainDom.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// unexpected (will be caught below)
|
||||
throw;
|
||||
}
|
||||
|
||||
var result = InsertLockRecord(tempId); //we change the row to a random Id to signal other MainDom to shutdown
|
||||
if (result == RecordPersistenceType.Insert)
|
||||
{
|
||||
// if we've inserted, then there was no MainDom so we can instantly acquire
|
||||
|
||||
// TODO: see the other TODO, could we just delete the row and that would indicate that we
|
||||
// are MainDom? then we don't leave any orphan rows behind.
|
||||
|
||||
InsertLockRecord(_lockId); // so update with our appdomain id
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// if we've updated, this means there is an active MainDom, now we need to wait to
|
||||
// for the current MainDom to shutdown which also requires releasing our write lock
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
// unexpected
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, cannot acquire MainDom");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
}
|
||||
|
||||
return await WaitForExistingAsync(tempId, millisecondsTimeout);
|
||||
}
|
||||
|
||||
public Task ListenAsync()
|
||||
{
|
||||
if (_hasError)
|
||||
{
|
||||
_logger.Warn<SqlMainDomLock>("Could not acquire MainDom, listening is canceled.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Create a long running task (dedicated thread)
|
||||
// to poll to check if we are still the MainDom registered in the DB
|
||||
return Task.Factory.StartNew(ListeningLoop, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||
|
||||
}
|
||||
|
||||
private void ListeningLoop()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// poll every 1 second
|
||||
Thread.Sleep(1000);
|
||||
|
||||
lock (_locker)
|
||||
{
|
||||
// If cancellation has been requested we will just exit. Depending on timing of the shutdown,
|
||||
// we will have already flagged _mainDomChanging = true, or we're shutting down faster than
|
||||
// the other MainDom is taking to startup. In this case the db row will just be deleted and the
|
||||
// new MainDom will just take over.
|
||||
if (_cancellationTokenSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var db = GetDatabase();
|
||||
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
// get a read lock
|
||||
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that
|
||||
// we are still the maindom. An empty value might be better because then we won't have any orphan rows
|
||||
// if the app is terminated. Could that work?
|
||||
|
||||
if (!IsMainDomValue(_lockId))
|
||||
{
|
||||
// we are no longer main dom, another one has come online, exit
|
||||
_mainDomChanging = true;
|
||||
_logger.Debug<SqlMainDomLock>("Detected new booting application, releasing MainDom lock.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
// unexpected
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, listening is canceled.");
|
||||
_hasError = true;
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetDatabase()
|
||||
{
|
||||
if (_db.InTransaction)
|
||||
_db.AbortTransaction();
|
||||
_db.Dispose();
|
||||
_db = null;
|
||||
}
|
||||
|
||||
private IUmbracoDatabase GetDatabase()
|
||||
{
|
||||
if (_db != null)
|
||||
return _db;
|
||||
|
||||
_db = _dbFactory.CreateDatabase();
|
||||
return _db;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for any existing MainDom to release so we can continue booting
|
||||
/// </summary>
|
||||
/// <param name="tempId"></param>
|
||||
/// <param name="millisecondsTimeout"></param>
|
||||
/// <returns></returns>
|
||||
private Task<bool> WaitForExistingAsync(string tempId, int millisecondsTimeout)
|
||||
{
|
||||
var updatedTempId = tempId + UpdatedSuffix;
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var db = GetDatabase();
|
||||
var watch = new Stopwatch();
|
||||
watch.Start();
|
||||
while(true)
|
||||
{
|
||||
// poll very often, we need to take over as fast as we can
|
||||
Thread.Sleep(100);
|
||||
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
// get a read lock
|
||||
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// the row
|
||||
var mainDomRows = db.Fetch<KeyValueDto>("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
|
||||
|
||||
if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId)
|
||||
{
|
||||
// the other main dom has updated our record
|
||||
// Or the other maindom shutdown super fast and just deleted the record
|
||||
// which indicates that we
|
||||
// can acquire it and it has shutdown.
|
||||
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// so now we update the row with our appdomain id
|
||||
InsertLockRecord(_lockId);
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId))
|
||||
{
|
||||
// in this case, the prefixed ID is different which means
|
||||
// another new AppDomain has come online and is wanting to take over. In that case, we will not
|
||||
// acquire.
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Cannot acquire, another booting application detected.");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
|
||||
if (IsLockTimeoutException(ex))
|
||||
{
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, waiting for existing MainDom is canceled.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
// unexpected
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, waiting for existing MainDom is canceled.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
}
|
||||
|
||||
if (watch.ElapsedMilliseconds >= millisecondsTimeout)
|
||||
{
|
||||
// if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown,
|
||||
// or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row
|
||||
// and it's just been left as an orphan row.
|
||||
// There's really know way of knowing unless we are constantly updating the row for the current maindom
|
||||
// which isn't ideal.
|
||||
// So... we're going to 'just' take over, if the writelock works then we'll assume we're ok
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Timeout elapsed, assuming orphan row, acquiring MainDom.");
|
||||
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// so now we update the row with our appdomain id
|
||||
InsertLockRecord(_lockId);
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
|
||||
if (IsLockTimeoutException(ex))
|
||||
{
|
||||
// something is wrong, we cannot acquire, not much we can do
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, could not forcibly acquire MainDom.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, could not forcibly acquire MainDom.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, _cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts or updates the key/value row
|
||||
/// </summary>
|
||||
private RecordPersistenceType InsertLockRecord(string id)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
return db.InsertOrUpdate(new KeyValueDto
|
||||
{
|
||||
Key = MainDomKey,
|
||||
Value = id,
|
||||
Updated = DateTime.Now
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the DB row value is equals the value
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool IsMainDomValue(string val)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
return db.ExecuteScalar<int>("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val",
|
||||
new { key = MainDomKey, val = val }) == 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the exception is an SQL timeout
|
||||
/// </summary>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
private bool IsLockTimeoutException(Exception exception) => exception is SqlException sqlException && sqlException.Number == 1222;
|
||||
|
||||
#region IDisposable Support
|
||||
private bool _disposedValue = false; // To detect redundant calls
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
// immediately cancel all sub-tasks, we don't want them to keep querying
|
||||
_cancellationTokenSource.Cancel();
|
||||
_cancellationTokenSource.Dispose();
|
||||
|
||||
var db = GetDatabase();
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
// get a write lock
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// When we are disposed, it means we have released the MainDom lock
|
||||
// and called all MainDom release callbacks, in this case
|
||||
// if another maindom is actually coming online we need
|
||||
// to signal to the MainDom coming online that we have shutdown.
|
||||
// To do that, we update the existing main dom DB record with a suffixed "_updated" string.
|
||||
// Otherwise, if we are just shutting down, we want to just delete the row.
|
||||
if (_mainDomChanging)
|
||||
{
|
||||
_logger.Debug<SqlMainDomLock>("Releasing MainDom, updating row, new application is booting.");
|
||||
db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey });
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug<SqlMainDomLock>("Releasing MainDom, deleting row, application is shutting down.");
|
||||
db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error during dipsose.");
|
||||
_hasError = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
ResetDatabase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
// This code added to correctly implement the disposable pattern.
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
|
||||
Dispose(true);
|
||||
}
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace Umbraco.Core.Scoping
|
||||
/// Provides scopes.
|
||||
/// </summary>
|
||||
public interface IScopeProvider
|
||||
{
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an ambient scope.
|
||||
/// </summary>
|
||||
|
||||
+9
@@ -369,6 +369,15 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasContentNodes(int id)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(ReadLockIds);
|
||||
return Repository.HasContentNodes(id);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Save
|
||||
|
||||
@@ -30,9 +30,8 @@ namespace Umbraco.TestData
|
||||
if (ct.Variations.VariesBySegment())
|
||||
return Content($"The document type {alias} already allows segments, nothing has been changed");
|
||||
|
||||
ct.Variations = ct.Variations.SetFlag(ContentVariation.Segment);
|
||||
|
||||
propType.Variations = propType.Variations.SetFlag(ContentVariation.Segment);
|
||||
ct.SetVariesBy(ContentVariation.Segment);
|
||||
propType.SetVariesBy(ContentVariation.Segment);
|
||||
|
||||
Services.ContentTypeService.Save(ct);
|
||||
return Content($"The document type {alias} and property type {propertyTypeAlias} now allows segments");
|
||||
@@ -50,7 +49,7 @@ namespace Umbraco.TestData
|
||||
if (!ct.VariesBySegment())
|
||||
return Content($"The document type {alias} does not allow segments, nothing has been changed");
|
||||
|
||||
ct.Variations = ct.Variations.UnsetFlag(ContentVariation.Segment);
|
||||
ct.SetVariesBy(ContentVariation.Segment, false);
|
||||
|
||||
Services.ContentTypeService.Save(ct);
|
||||
return Content($"The document type {alias} no longer allows segments");
|
||||
|
||||
@@ -223,7 +223,7 @@ namespace Umbraco.Tests.Cache
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
@@ -321,7 +321,7 @@ namespace Umbraco.Tests.Cache
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
|
||||
Assert.AreEqual(0, d.Test.GetValues(1).Length);
|
||||
|
||||
// gen 1
|
||||
@@ -416,7 +416,7 @@ namespace Umbraco.Tests.Cache
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
@@ -578,7 +578,7 @@ namespace Umbraco.Tests.Cache
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
|
||||
d.Set(1, "one");
|
||||
d.Set(2, "two");
|
||||
|
||||
@@ -689,7 +689,7 @@ namespace Umbraco.Tests.Cache
|
||||
{
|
||||
// gen 3
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "ein");
|
||||
d.SetLocked(1, "ein");
|
||||
Assert.AreEqual(3, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
@@ -727,31 +727,25 @@ namespace Umbraco.Tests.Cache
|
||||
using (var w1 = d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
Assert.AreEqual(1, t.LiveGen);
|
||||
Assert.AreEqual(1, t.WLocked);
|
||||
Assert.IsTrue(t.IsLocked);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
|
||||
using (var w2 = d.GetScopedWriteLock(scopeProvider))
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
Assert.AreEqual(1, t.LiveGen);
|
||||
Assert.AreEqual(2, t.WLocked);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
|
||||
Assert.AreNotSame(w1, w2); // get a new writer each time
|
||||
|
||||
d.Set(1, "one");
|
||||
|
||||
Assert.AreEqual(0, d.CreateSnapshot().Gen);
|
||||
}
|
||||
using (var w2 = d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
Assert.AreEqual(1, t.LiveGen);
|
||||
Assert.AreEqual(1, t.WLocked);
|
||||
Assert.IsTrue(t.IsLocked);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
|
||||
Assert.AreEqual(0, d.CreateSnapshot().Gen);
|
||||
}
|
||||
|
||||
Assert.AreEqual(1, t.LiveGen);
|
||||
Assert.AreEqual(0, t.WLocked);
|
||||
Assert.IsFalse(t.IsLocked);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
|
||||
Assert.AreEqual(1, d.CreateSnapshot().Gen);
|
||||
@@ -772,11 +766,14 @@ namespace Umbraco.Tests.Cache
|
||||
|
||||
using (var w1 = d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
// This one is interesting, although we don't allow recursive locks, since this is
|
||||
// using the same ScopeContext/key, the lock acquisition is only done once
|
||||
|
||||
using (var w2 = d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
Assert.AreSame(w1, w2);
|
||||
|
||||
d.Set(1, "one");
|
||||
d.SetLocked(1, "one");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -797,19 +794,16 @@ namespace Umbraco.Tests.Cache
|
||||
using (var w1 = d.GetScopedWriteLock(scopeProvider1))
|
||||
{
|
||||
Assert.AreEqual(1, t.LiveGen);
|
||||
Assert.AreEqual(1, t.WLocked);
|
||||
Assert.IsTrue(t.IsLocked);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
|
||||
using (var w2 = d.GetScopedWriteLock(scopeProvider2))
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
Assert.AreEqual(1, t.LiveGen);
|
||||
Assert.AreEqual(2, t.WLocked);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
using (var w2 = d.GetScopedWriteLock(scopeProvider2))
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
Assert.AreNotSame(w1, w2);
|
||||
|
||||
d.Set(1, "one");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -848,13 +842,13 @@ namespace Umbraco.Tests.Cache
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
Assert.AreEqual("uno", s2.Get(1));
|
||||
|
||||
var scopeProvider = GetScopeProvider();
|
||||
|
||||
var scopeProvider = GetScopeProvider();
|
||||
using (d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
// gen 3
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "ein");
|
||||
d.SetLocked(1, "ein");
|
||||
Assert.AreEqual(3, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
@@ -881,6 +875,7 @@ namespace Umbraco.Tests.Cache
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
@@ -894,12 +889,11 @@ namespace Umbraco.Tests.Cache
|
||||
Assert.AreEqual("uno", s2.Get(1));
|
||||
|
||||
var scopeProvider = GetScopeProvider();
|
||||
|
||||
using (d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
// creating a snapshot in a write-lock does NOT return the "current" content
|
||||
// it uses the previous snapshot, so new snapshot created only on release
|
||||
d.Set(1, "ein");
|
||||
d.SetLocked(1, "ein");
|
||||
var s3 = d.CreateSnapshot();
|
||||
Assert.AreEqual(2, s3.Gen);
|
||||
Assert.AreEqual("uno", s3.Get(1));
|
||||
@@ -934,12 +928,11 @@ namespace Umbraco.Tests.Cache
|
||||
|
||||
var scopeContext = new ScopeContext();
|
||||
var scopeProvider = GetScopeProvider(scopeContext);
|
||||
|
||||
using (d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
// creating a snapshot in a write-lock does NOT return the "current" content
|
||||
// it uses the previous snapshot, so new snapshot created only on release
|
||||
d.Set(1, "ein");
|
||||
d.SetLocked(1, "ein");
|
||||
var s3 = d.CreateSnapshot();
|
||||
Assert.AreEqual(2, s3.Gen);
|
||||
Assert.AreEqual("uno", s3.Get(1));
|
||||
@@ -967,7 +960,7 @@ namespace Umbraco.Tests.Cache
|
||||
var d = new SnapDictionary<int, string>();
|
||||
var t = d.Test;
|
||||
t.CollectAuto = false;
|
||||
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
var s1 = d.CreateSnapshot();
|
||||
@@ -984,12 +977,11 @@ namespace Umbraco.Tests.Cache
|
||||
|
||||
var scopeContext = new ScopeContext();
|
||||
var scopeProvider = GetScopeProvider(scopeContext);
|
||||
|
||||
using (d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
// creating a snapshot in a write-lock does NOT return the "current" content
|
||||
// it uses the previous snapshot, so new snapshot created only on release
|
||||
d.Set(1, "ein");
|
||||
d.SetLocked(1, "ein");
|
||||
var s3 = d.CreateSnapshot();
|
||||
Assert.AreEqual(2, s3.Gen);
|
||||
Assert.AreEqual("uno", s3.Get(1));
|
||||
@@ -997,7 +989,7 @@ namespace Umbraco.Tests.Cache
|
||||
// we made some changes, so a next gen is required
|
||||
Assert.AreEqual(3, t.LiveGen);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
Assert.AreEqual(1, t.WLocked);
|
||||
Assert.IsTrue(t.IsLocked);
|
||||
|
||||
// but live snapshot contains changes
|
||||
var ls = t.LiveSnapshot;
|
||||
@@ -1008,7 +1000,7 @@ namespace Umbraco.Tests.Cache
|
||||
// nothing is committed until scope exits
|
||||
Assert.AreEqual(3, t.LiveGen);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
Assert.AreEqual(1, t.WLocked);
|
||||
Assert.IsTrue(t.IsLocked);
|
||||
|
||||
// no changes until exit
|
||||
var s4 = d.CreateSnapshot();
|
||||
@@ -1020,7 +1012,7 @@ namespace Umbraco.Tests.Cache
|
||||
// now things have changed
|
||||
Assert.AreEqual(2, t.LiveGen);
|
||||
Assert.IsFalse(t.NextGen);
|
||||
Assert.AreEqual(0, t.WLocked);
|
||||
Assert.IsFalse(t.IsLocked);
|
||||
|
||||
// no changes since not completed
|
||||
var s5 = d.CreateSnapshot();
|
||||
@@ -1097,9 +1089,10 @@ namespace Umbraco.Tests.Cache
|
||||
// writer is scope contextual and scoped
|
||||
// when disposed, nothing happens
|
||||
// when the context exists, the writer is released
|
||||
|
||||
using (d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
d.Set(1, "ein");
|
||||
d.SetLocked(1, "ein");
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsNotNull(d.Test.GenObj);
|
||||
@@ -1107,7 +1100,7 @@ namespace Umbraco.Tests.Cache
|
||||
}
|
||||
|
||||
// writer has not released
|
||||
Assert.AreEqual(1, d.Test.WLocked);
|
||||
Assert.IsTrue(d.Test.IsLocked);
|
||||
Assert.IsNotNull(d.Test.GenObj);
|
||||
Assert.AreEqual(2, d.Test.GenObj.Gen);
|
||||
|
||||
@@ -1118,7 +1111,7 @@ namespace Umbraco.Tests.Cache
|
||||
// panic!
|
||||
var s2 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(1, d.Test.WLocked);
|
||||
Assert.IsTrue(d.Test.IsLocked);
|
||||
Assert.IsNotNull(d.Test.GenObj);
|
||||
Assert.AreEqual(2, d.Test.GenObj.Gen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
@@ -1127,7 +1120,7 @@ namespace Umbraco.Tests.Cache
|
||||
// release writer
|
||||
scopeContext.ScopeExit(true);
|
||||
|
||||
Assert.AreEqual(0, d.Test.WLocked);
|
||||
Assert.IsFalse(d.Test.IsLocked);
|
||||
Assert.IsNotNull(d.Test.GenObj);
|
||||
Assert.AreEqual(2, d.Test.GenObj.Gen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
@@ -1135,7 +1128,7 @@ namespace Umbraco.Tests.Cache
|
||||
|
||||
var s3 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(0, d.Test.WLocked);
|
||||
Assert.IsFalse(d.Test.IsLocked);
|
||||
Assert.IsNotNull(d.Test.GenObj);
|
||||
Assert.AreEqual(3, d.Test.GenObj.Gen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
@@ -1150,4 +1143,45 @@ namespace Umbraco.Tests.Cache
|
||||
return scopeProvider;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used for tests so that we don't have to wrap every Set/Clear call in locks
|
||||
/// </summary>
|
||||
public static class SnapDictionaryExtensions
|
||||
{
|
||||
internal static void Set<TKey, TValue>(this SnapDictionary<TKey, TValue> d, TKey key, TValue value)
|
||||
where TValue : class
|
||||
{
|
||||
using (d.GetScopedWriteLock(GetScopeProvider()))
|
||||
{
|
||||
d.SetLocked(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Clear<TKey, TValue>(this SnapDictionary<TKey, TValue> d)
|
||||
where TValue : class
|
||||
{
|
||||
using (d.GetScopedWriteLock(GetScopeProvider()))
|
||||
{
|
||||
d.ClearLocked();
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Clear<TKey, TValue>(this SnapDictionary<TKey, TValue> d, TKey key)
|
||||
where TValue : class
|
||||
{
|
||||
using (d.GetScopedWriteLock(GetScopeProvider()))
|
||||
{
|
||||
d.ClearLocked(key);
|
||||
}
|
||||
}
|
||||
|
||||
private static IScopeProvider GetScopeProvider()
|
||||
{
|
||||
var scopeProvider = Mock.Of<IScopeProvider>();
|
||||
Mock.Get(scopeProvider)
|
||||
.Setup(x => x.Context).Returns(() => null);
|
||||
return scopeProvider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Web.Trees;
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Web.Trees;
|
||||
|
||||
namespace Umbraco.Tests.CoreThings
|
||||
{
|
||||
@@ -22,6 +23,7 @@ namespace Umbraco.Tests.CoreThings
|
||||
Assert.IsFalse(value.HasFlag(test));
|
||||
}
|
||||
|
||||
[Obsolete]
|
||||
[TestCase(TreeUse.Dialog, TreeUse.Dialog, true)]
|
||||
[TestCase(TreeUse.Dialog, TreeUse.Main, false)]
|
||||
[TestCase(TreeUse.Dialog | TreeUse.Main, TreeUse.Dialog, true)]
|
||||
@@ -51,47 +53,5 @@ namespace Umbraco.Tests.CoreThings
|
||||
else
|
||||
Assert.IsFalse(value.HasFlagAny(test));
|
||||
}
|
||||
|
||||
[TestCase(TreeUse.None, TreeUse.None, TreeUse.None)]
|
||||
[TestCase(TreeUse.None, TreeUse.Main, TreeUse.Main)]
|
||||
[TestCase(TreeUse.None, TreeUse.Dialog, TreeUse.Dialog)]
|
||||
[TestCase(TreeUse.None, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)]
|
||||
[TestCase(TreeUse.Main, TreeUse.None, TreeUse.Main)]
|
||||
[TestCase(TreeUse.Main, TreeUse.Main, TreeUse.Main)]
|
||||
[TestCase(TreeUse.Main, TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)]
|
||||
[TestCase(TreeUse.Main, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)]
|
||||
[TestCase(TreeUse.Dialog, TreeUse.None, TreeUse.Dialog)]
|
||||
[TestCase(TreeUse.Dialog, TreeUse.Main, TreeUse.Main | TreeUse.Dialog)]
|
||||
[TestCase(TreeUse.Dialog, TreeUse.Dialog, TreeUse.Dialog)]
|
||||
[TestCase(TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)]
|
||||
[TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.None, TreeUse.Main | TreeUse.Dialog)]
|
||||
[TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main, TreeUse.Main | TreeUse.Dialog)]
|
||||
[TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)]
|
||||
[TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)]
|
||||
public void SetFlagTests(TreeUse value, TreeUse flag, TreeUse expected)
|
||||
{
|
||||
Assert.AreEqual(expected, value.SetFlag(flag));
|
||||
}
|
||||
|
||||
[TestCase(TreeUse.None, TreeUse.None, TreeUse.None)]
|
||||
[TestCase(TreeUse.None, TreeUse.Main, TreeUse.None)]
|
||||
[TestCase(TreeUse.None, TreeUse.Dialog, TreeUse.None)]
|
||||
[TestCase(TreeUse.None, TreeUse.Main | TreeUse.Dialog, TreeUse.None)]
|
||||
[TestCase(TreeUse.Main, TreeUse.None, TreeUse.Main)]
|
||||
[TestCase(TreeUse.Main, TreeUse.Main, TreeUse.None)]
|
||||
[TestCase(TreeUse.Main, TreeUse.Dialog, TreeUse.Main)]
|
||||
[TestCase(TreeUse.Main, TreeUse.Main | TreeUse.Dialog, TreeUse.None)]
|
||||
[TestCase(TreeUse.Dialog, TreeUse.None, TreeUse.Dialog)]
|
||||
[TestCase(TreeUse.Dialog, TreeUse.Main, TreeUse.Dialog)]
|
||||
[TestCase(TreeUse.Dialog, TreeUse.Dialog, TreeUse.None)]
|
||||
[TestCase(TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.None)]
|
||||
[TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.None, TreeUse.Main | TreeUse.Dialog)]
|
||||
[TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main, TreeUse.Dialog)]
|
||||
[TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Dialog, TreeUse.Main)]
|
||||
[TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.None)]
|
||||
public void UnsetFlagTests(TreeUse value, TreeUse flag, TreeUse expected)
|
||||
{
|
||||
Assert.AreEqual(expected, value.UnsetFlag(flag));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.Persistence.Repositories;
|
||||
using Umbraco.Core.Runtime;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Strings;
|
||||
|
||||
@@ -15,6 +15,7 @@ using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Persistence;
|
||||
using Umbraco.Core.Persistence.Repositories;
|
||||
using Umbraco.Core.Persistence.Repositories.Implement;
|
||||
using Umbraco.Core.Runtime;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Services.Changes;
|
||||
|
||||
@@ -971,6 +971,32 @@ namespace Umbraco.Tests.Persistence.Repositories
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Can_Verify_Content_Type_Has_Content_Nodes()
|
||||
{
|
||||
// Arrange
|
||||
var provider = TestObjects.GetScopeProvider(Logger);
|
||||
using (var scope = provider.CreateScope())
|
||||
{
|
||||
ContentTypeRepository repository;
|
||||
var contentRepository = CreateRepository((IScopeAccessor)provider, out repository);
|
||||
var contentTypeId = NodeDto.NodeIdSeed + 1;
|
||||
var contentType = repository.Get(contentTypeId);
|
||||
|
||||
// Act
|
||||
var result = repository.HasContentNodes(contentTypeId);
|
||||
|
||||
var subpage = MockedContent.CreateTextpageContent(contentType, "Test Page 1", contentType.Id);
|
||||
contentRepository.Save(subpage);
|
||||
|
||||
var result2 = repository.HasContentNodes(contentTypeId);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.False);
|
||||
Assert.That(result2, Is.True);
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateTestData()
|
||||
{
|
||||
//Create and Save ContentType "umbTextpage" -> (NodeDto.NodeIdSeed)
|
||||
|
||||
@@ -925,6 +925,14 @@ namespace Umbraco.Tests.PublishedContent
|
||||
var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null);
|
||||
_snapshotAccessor.PublishedSnapshot = snapshot;
|
||||
|
||||
var snapshotService = (PublishedSnapshotService)_snapshotService;
|
||||
var contentStore = snapshotService.GetContentStore();
|
||||
|
||||
var parentNodes = contentStore.Test.GetValues(1);
|
||||
var parentNode = parentNodes[0];
|
||||
AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6);
|
||||
Assert.AreEqual(1, parentNode.gen);
|
||||
|
||||
var documents = snapshot.Content.GetAtRoot().ToArray();
|
||||
AssertDocuments(documents, "N1", "N2", "N3");
|
||||
|
||||
@@ -941,6 +949,15 @@ namespace Umbraco.Tests.PublishedContent
|
||||
new ContentCacheRefresher.JsonPayload(2, Guid.Empty, TreeChangeTypes.RefreshNode),
|
||||
}, out _, out _);
|
||||
|
||||
parentNodes = contentStore.Test.GetValues(1);
|
||||
Assert.AreEqual(2, parentNodes.Length);
|
||||
parentNode = parentNodes[1]; // get the first gen
|
||||
AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); // the structure should have remained the same
|
||||
Assert.AreEqual(1, parentNode.gen);
|
||||
parentNode = parentNodes[0]; // get the latest gen
|
||||
AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); // the structure should have remained the same
|
||||
Assert.AreEqual(2, parentNode.gen);
|
||||
|
||||
documents = snapshot.Content.GetAtRoot().ToArray();
|
||||
AssertDocuments(documents, "N1", "N2", "N3");
|
||||
|
||||
@@ -949,6 +966,8 @@ namespace Umbraco.Tests.PublishedContent
|
||||
|
||||
documents = snapshot.Content.GetById(2).Children(_variationAccesor).ToArray();
|
||||
AssertDocuments(documents, "N9", "N8", "N7");
|
||||
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -22,6 +22,7 @@ using Umbraco.Core.Models.Entities;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.Persistence;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Runtime;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Strings;
|
||||
using Umbraco.Core.Sync;
|
||||
@@ -96,7 +97,7 @@ namespace Umbraco.Tests.TestHelpers
|
||||
|
||||
|
||||
public static IIOHelper IOHelper { get; } = new IOHelper(GetHostingEnvironment());
|
||||
public static IMainDom MainDom { get; } = new MainDom(Mock.Of<ILogger>(), GetHostingEnvironment());
|
||||
public static IMainDom MainDom { get; } = new MainDom(Mock.Of<ILogger>(), GetHostingEnvironment(), new MainDomSemaphoreLock(Mock.Of<ILogger>(), GetHostingEnvironment()));
|
||||
/// <summary>
|
||||
/// Maps the given <paramref name="relativePath"/> making it rooted on <see cref="CurrentAssemblyDirectory"/>. <paramref name="relativePath"/> must start with <code>~/</code>
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<configuration>
|
||||
<configSections>
|
||||
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
|
||||
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
|
||||
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
|
||||
<system.web.webPages.razor>
|
||||
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.7.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
|
||||
<pages pageBaseType="System.Web.Mvc.WebViewPage">
|
||||
<namespaces>
|
||||
<add namespace="System.Web.Mvc" />
|
||||
<add namespace="System.Web.Mvc.Ajax" />
|
||||
<add namespace="System.Web.Mvc.Html" />
|
||||
<add namespace="System.Web.Routing" />
|
||||
<add namespace="Umbraco.Web" />
|
||||
<add namespace="Umbraco.Core" />
|
||||
<add namespace="Umbraco.Core.Models" />
|
||||
<add namespace="Umbraco.Core.Models.PublishedContent" />
|
||||
<add namespace="Umbraco.Web.Mvc" />
|
||||
<add namespace="Examine" />
|
||||
<add namespace="Umbraco.Web.PublishedModels" />
|
||||
</namespaces>
|
||||
</pages>
|
||||
</system.web.webPages.razor>
|
||||
|
||||
<appSettings>
|
||||
<add key="webpages:Enabled" value="false" />
|
||||
</appSettings>
|
||||
|
||||
<system.webServer>
|
||||
<handlers>
|
||||
<remove name="BlockViewHandler"/>
|
||||
<add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
|
||||
</handlers>
|
||||
</system.webServer>
|
||||
|
||||
<system.web>
|
||||
<compilation targetFramework="4.7.2">
|
||||
<assemblies>
|
||||
<add assembly="System.Web.Mvc, Version=5.2.7.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
|
||||
|
||||
<!--
|
||||
Views are compiled by the Microsoft.CodeDom.Providers.DotNetCompilerPlatform which uses the Roslyn compiler
|
||||
from ~/bin/roslyn to expose an ICodeProvider implementation to System.CodeDom.
|
||||
|
||||
For instance, starting with package 1.4.0 (which contains version 1.2.2.0) System.Collections.Immutable
|
||||
lists netstandard2.0 as a supported target, whereas package 1.3.1 only listed net45. So now, when
|
||||
installing, NuGet picks the netstandard2.0 version, thus introducing a dependency to netstandard2.0.
|
||||
|
||||
Somehow, transitive dependencies are not working correctly, and either (a) NuGet fails to properly
|
||||
register this dependency, or (b) when reference assemblies are gathered before compiling views, this
|
||||
dependency is missed. In any case, the result is that the ICodeProvider is passed a list of referenced
|
||||
assemblies that is missing .NET Standard.
|
||||
|
||||
It may be a mix of both. NuGet registers the dependency well enough, that we can actually build the
|
||||
whole application - but it is not doing something that is required in order to have .NET Standard
|
||||
being a dependency when building views.
|
||||
|
||||
See also: https://stackoverflow.com/questions/50165910
|
||||
|
||||
Funny enough, the CSharpCompiler already explicitly adds System.Runtime as a referenced assembly,
|
||||
with a comment mentioning an issue. But it's not adding .NET Standard. So, for the time being, to be sure,
|
||||
we are adding both here.
|
||||
-->
|
||||
<add assembly="System.Runtime, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||
<add assembly="netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51" />
|
||||
</assemblies>
|
||||
</compilation>
|
||||
</system.web>
|
||||
</configuration>
|
||||
@@ -132,6 +132,10 @@ ol.inline {
|
||||
display: inline-block;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
|
||||
&.-no-padding-left{
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
* its own image insertion dialog, this hook should return true, and the callback should be called with the chosen
|
||||
* image url (or null if the user cancelled). If this hook returns false, the default dialog will be used.
|
||||
*/
|
||||
hooks.addFalse("insertLinkDialog");
|
||||
|
||||
this.getConverter = function () { return markdownConverter; }
|
||||
|
||||
@@ -1636,7 +1637,7 @@
|
||||
var that = this;
|
||||
// The function to be executed when you enter a link and press OK or Cancel.
|
||||
// Marks up the link and adds the ref.
|
||||
var linkEnteredCallback = function (link) {
|
||||
var linkEnteredCallback = function (link, title) {
|
||||
|
||||
if (link !== null) {
|
||||
// ( $1
|
||||
@@ -1667,10 +1668,10 @@
|
||||
|
||||
if (!chunk.selection) {
|
||||
if (isImage) {
|
||||
chunk.selection = "enter image description here";
|
||||
chunk.selection = title || "enter image description here";
|
||||
}
|
||||
else {
|
||||
chunk.selection = "enter link description here";
|
||||
chunk.selection = title || "enter link description here";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1683,7 +1684,8 @@
|
||||
ui.prompt('Insert Image', imageDialogText, imageDefaultText, linkEnteredCallback);
|
||||
}
|
||||
else {
|
||||
ui.prompt('Insert Link', linkDialogText, linkDefaultText, linkEnteredCallback);
|
||||
if (!this.hooks.insertLinkDialog(linkEnteredCallback))
|
||||
ui.prompt('Insert Link', linkDialogText, linkDefaultText, linkEnteredCallback);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -791,6 +791,7 @@
|
||||
$scope.content.variants[i].expireDate = model.variants[i].expireDate;
|
||||
$scope.content.variants[i].releaseDateFormatted = model.variants[i].releaseDateFormatted;
|
||||
$scope.content.variants[i].expireDateFormatted = model.variants[i].expireDateFormatted;
|
||||
$scope.content.variants[i].save = model.variants[i].save;
|
||||
}
|
||||
|
||||
model.submitButtonState = "busy";
|
||||
|
||||
+2
-2
@@ -17,8 +17,8 @@
|
||||
scope.isNew = scope.content.state == "NotCreated";
|
||||
|
||||
localizationService.localizeMany([
|
||||
scope.isNew ? "placeholders_a11yCreateItem" : "placeholders_a11yEdit",
|
||||
"placeholders_a11yName",
|
||||
scope.isNew ? "visuallyHiddenTexts_createItem" : "visuallyHiddenTexts_edit",
|
||||
"visuallyHiddenTexts_name",
|
||||
scope.isNew ? "general_new" : "general_edit"]
|
||||
).then(function (data) {
|
||||
|
||||
|
||||
+2
-2
@@ -233,8 +233,8 @@ Use this directive to construct a header inside the main editor window.
|
||||
editorState.current.id === "-1";
|
||||
|
||||
var localizeVars = [
|
||||
scope.isNew ? "placeholders_a11yCreateItem" : "placeholders_a11yEdit",
|
||||
"placeholders_a11yName",
|
||||
scope.isNew ? "visuallyHiddenTexts_createItem" : "visuallyHiddenTexts_edit",
|
||||
"visuallyHiddenTexts_name",
|
||||
scope.isNew ? "general_new" : "general_edit"
|
||||
];
|
||||
|
||||
|
||||
+10
-1
@@ -213,7 +213,6 @@ Opens an overlay to show a custom YSOD. </br>
|
||||
var unsubscribe = [];
|
||||
|
||||
function activate() {
|
||||
|
||||
setView();
|
||||
|
||||
setButtonText();
|
||||
@@ -247,10 +246,20 @@ Opens an overlay to show a custom YSOD. </br>
|
||||
|
||||
setOverlayIndent();
|
||||
|
||||
focusOnOverlayHeading()
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Ideally this would focus on the first natively focusable element in the overlay, but as the content can be dynamic, it is focusing on the heading.
|
||||
function focusOnOverlayHeading() {
|
||||
var heading = el.find(".umb-overlay__title");
|
||||
|
||||
if(heading) {
|
||||
heading.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function setView() {
|
||||
|
||||
if (scope.view) {
|
||||
|
||||
+18
-1
@@ -188,6 +188,22 @@ Use this directive to render a ui component for selecting child items to a paren
|
||||
syncParentIcon();
|
||||
}));
|
||||
|
||||
// sortable options for allowed child content types
|
||||
scope.sortableOptions = {
|
||||
axis: "y",
|
||||
containment: "parent",
|
||||
distance: 10,
|
||||
opacity: 0.7,
|
||||
tolerance: "pointer",
|
||||
scroll: true,
|
||||
zIndex: 6000,
|
||||
update: function (e, ui) {
|
||||
if(scope.onSort) {
|
||||
scope.onSort();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// clean up
|
||||
scope.$on('$destroy', function(){
|
||||
// unbind watchers
|
||||
@@ -209,7 +225,8 @@ Use this directive to render a ui component for selecting child items to a paren
|
||||
parentIcon: "=",
|
||||
parentId: "=",
|
||||
onRemove: "=",
|
||||
onAdd: "="
|
||||
onAdd: "=",
|
||||
onSort: "="
|
||||
},
|
||||
link: link
|
||||
};
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* Konami Code directive for AngularJS
|
||||
* @version v0.0.1
|
||||
* @license MIT License, https://www.opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
angular.module('umbraco.directives')
|
||||
.directive('konamiCode', ['$document', function ($document) {
|
||||
var konamiKeysDefault = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attr) {
|
||||
|
||||
if (!attr.konamiCode) {
|
||||
throw ('Konami directive must receive an expression as value.');
|
||||
}
|
||||
|
||||
// Let user define a custom code.
|
||||
var konamiKeys = attr.konamiKeys || konamiKeysDefault;
|
||||
var keyIndex = 0;
|
||||
|
||||
/**
|
||||
* Fired when konami code is type.
|
||||
*/
|
||||
function activated() {
|
||||
if ('konamiOnce' in attr) {
|
||||
stopListening();
|
||||
}
|
||||
// Execute expression.
|
||||
scope.$eval(attr.konamiCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keydown events.
|
||||
*/
|
||||
function keydown(e) {
|
||||
if (e.keyCode === konamiKeys[keyIndex++]) {
|
||||
if (keyIndex === konamiKeys.length) {
|
||||
keyIndex = 0;
|
||||
activated();
|
||||
}
|
||||
} else {
|
||||
keyIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop to listen typing.
|
||||
*/
|
||||
function stopListening() {
|
||||
$document.off('keydown', keydown);
|
||||
}
|
||||
|
||||
// Start listening to key typing.
|
||||
$document.on('keydown', keydown);
|
||||
|
||||
// Stop listening when scope is destroyed.
|
||||
scope.$on('$destroy', stopListening);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@@ -351,6 +351,16 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, loca
|
||||
return umbRequestHelper.resourcePromise(
|
||||
$http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCreateDefaultTemplate", { id: id })),
|
||||
'Failed to create default template for content type with id ' + id);
|
||||
},
|
||||
|
||||
hasContentNodes: function (id) {
|
||||
return umbRequestHelper.resourcePromise(
|
||||
$http.get(
|
||||
umbRequestHelper.getApiUrl(
|
||||
"contentTypeApiBaseUrl",
|
||||
"HasContentNodes",
|
||||
[{ id: id }])),
|
||||
'Failed to retrieve indication for whether content type with id ' + id + ' has associated content nodes');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,6 +127,25 @@ function entityResource($q, $http, umbRequestHelper) {
|
||||
'Failed to retrieve url for id:' + id);
|
||||
},
|
||||
|
||||
getUrlByUdi: function (udi, culture) {
|
||||
|
||||
if (!udi) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!culture) {
|
||||
culture = "";
|
||||
}
|
||||
|
||||
return umbRequestHelper.resourcePromise(
|
||||
$http.get(
|
||||
umbRequestHelper.getApiUrl(
|
||||
"entityApiBaseUrl",
|
||||
"GetUrl",
|
||||
[{ udi: udi }, {culture: culture }])),
|
||||
'Failed to retrieve url for UDI:' + udi);
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name umbraco.resources.entityResource#getById
|
||||
@@ -166,18 +185,22 @@ function entityResource($q, $http, umbRequestHelper) {
|
||||
},
|
||||
|
||||
|
||||
getUrlAndAnchors: function (id) {
|
||||
getUrlAndAnchors: function (id, culture) {
|
||||
|
||||
if (id === -1 || id === "-1") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!culture) {
|
||||
culture = "";
|
||||
}
|
||||
|
||||
return umbRequestHelper.resourcePromise(
|
||||
$http.get(
|
||||
umbRequestHelper.getApiUrl(
|
||||
"entityApiBaseUrl",
|
||||
"GetUrlAndAnchors",
|
||||
[{ id: id }])),
|
||||
[{ id: id }, {culture: culture }])),
|
||||
'Failed to retrieve url and anchors data for id ' + id);
|
||||
},
|
||||
|
||||
|
||||
@@ -20,10 +20,21 @@
|
||||
"GetTours")),
|
||||
'Failed to get tours');
|
||||
}
|
||||
|
||||
function getToursForDoctype(doctypeAlias) {
|
||||
return umbRequestHelper.resourcePromise(
|
||||
$http.get(
|
||||
umbRequestHelper.getApiUrl(
|
||||
"tourApiBaseUrl",
|
||||
"GetToursForDoctype",
|
||||
[{ doctypeAlias: doctypeAlias }])),
|
||||
'Failed to get tours');
|
||||
}
|
||||
|
||||
|
||||
var resource = {
|
||||
getTours: getTours
|
||||
getTours: getTours,
|
||||
getToursForDoctype: getToursForDoctype
|
||||
};
|
||||
|
||||
return resource;
|
||||
|
||||
@@ -640,6 +640,23 @@ When building a custom infinite editor view you can use the same components as a
|
||||
editor.view = "views/mediatypes/edit.html";
|
||||
open(editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name umbraco.services.editorService#memberTypeEditor
|
||||
* @methodOf umbraco.services.editorService
|
||||
*
|
||||
* @description
|
||||
* Opens the member type editor in infinite editing, the submit callback returns the saved member type
|
||||
* @param {Object} editor rendering options
|
||||
* @param {Callback} editor.submit Submits the editor
|
||||
* @param {Callback} editor.close Closes the editor
|
||||
* @returns {Object} editor object
|
||||
*/
|
||||
function memberTypeEditor(editor) {
|
||||
editor.view = "views/membertypes/edit.html";
|
||||
open(editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
@@ -1011,6 +1028,7 @@ When building a custom infinite editor view you can use the same components as a
|
||||
iconPicker: iconPicker,
|
||||
documentTypeEditor: documentTypeEditor,
|
||||
mediaTypeEditor: mediaTypeEditor,
|
||||
memberTypeEditor: memberTypeEditor,
|
||||
queryBuilder: queryBuilder,
|
||||
treePicker: treePicker,
|
||||
nodePermissions: nodePermissions,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
*
|
||||
* it is possible to modify this object, so should be used with care
|
||||
*/
|
||||
angular.module('umbraco.services').factory("editorState", function ($rootScope) {
|
||||
angular.module('umbraco.services').factory("editorState", function ($rootScope, eventsService) {
|
||||
|
||||
var current = null;
|
||||
|
||||
@@ -30,6 +30,7 @@ angular.module('umbraco.services').factory("editorState", function ($rootScope)
|
||||
*/
|
||||
set: function (entity) {
|
||||
current = entity;
|
||||
eventsService.emit("editorState.changed", { entity: entity });
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -134,36 +134,38 @@
|
||||
var groupedTours = [];
|
||||
tours.forEach(function (item) {
|
||||
|
||||
var groupExists = false;
|
||||
var newGroup = {
|
||||
"group": "",
|
||||
"tours": []
|
||||
};
|
||||
if (item.contentType === null || item.contentType === '') {
|
||||
var groupExists = false;
|
||||
var newGroup = {
|
||||
"group": "",
|
||||
"tours": []
|
||||
};
|
||||
|
||||
groupedTours.forEach(function(group){
|
||||
// extend existing group if it is already added
|
||||
if(group.group === item.group) {
|
||||
if(item.groupOrder) {
|
||||
group.groupOrder = item.groupOrder
|
||||
}
|
||||
groupExists = true;
|
||||
groupedTours.forEach(function (group) {
|
||||
// extend existing group if it is already added
|
||||
if (group.group === item.group) {
|
||||
if (item.groupOrder) {
|
||||
group.groupOrder = item.groupOrder;
|
||||
}
|
||||
groupExists = true;
|
||||
|
||||
if(item.hidden === false){
|
||||
group.tours.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// push new group to array if it doesn't exist
|
||||
if(!groupExists) {
|
||||
newGroup.group = item.group;
|
||||
if(item.groupOrder) {
|
||||
newGroup.groupOrder = item.groupOrder
|
||||
}
|
||||
// push new group to array if it doesn't exist
|
||||
if (!groupExists) {
|
||||
newGroup.group = item.group;
|
||||
if (item.groupOrder) {
|
||||
newGroup.groupOrder = item.groupOrder;
|
||||
}
|
||||
|
||||
if(item.hidden === false){
|
||||
newGroup.tours.push(item);
|
||||
groupedTours.push(newGroup);
|
||||
if(item.hidden === false){
|
||||
newGroup.tours.push(item);
|
||||
groupedTours.push(newGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +196,24 @@
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name umbraco.services.tourService#getToursForDoctype
|
||||
* @methodOf umbraco.services.tourService
|
||||
*
|
||||
* @description
|
||||
* Returns a promise of the tours found by documenttype alias.
|
||||
* @param {Object} doctypeAlias The doctype alias for which the tours which should be returned
|
||||
* @returns {Array} An array of tour objects for the doctype
|
||||
*/
|
||||
function getToursForDoctype(doctypeAlias) {
|
||||
var deferred = $q.defer();
|
||||
tourResource.getToursForDoctype(doctypeAlias).then(function (tours) {
|
||||
deferred.resolve(tours);
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
///////////
|
||||
|
||||
/**
|
||||
@@ -275,7 +295,8 @@
|
||||
completeTour: completeTour,
|
||||
getCurrentTour: getCurrentTour,
|
||||
getGroupedTours: getGroupedTours,
|
||||
getTourByAlias: getTourByAlias
|
||||
getTourByAlias: getTourByAlias,
|
||||
getToursForDoctype : getToursForDoctype
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,.12),0 6px 6px rgba(0,0,0,.14);
|
||||
}
|
||||
|
||||
.umb-search__label{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Search field
|
||||
*/
|
||||
@@ -107,4 +111,4 @@
|
||||
.umb-search-result__description {
|
||||
color: @gray-5;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
|
||||
.umb-child-selector__children-container {
|
||||
margin-left: 30px;
|
||||
.umb-child-selector__child {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
.umb-child-selector__child-description {
|
||||
|
||||
@@ -162,6 +162,8 @@
|
||||
}
|
||||
|
||||
.umb-grid .umb-row .umb-cell-placeholder {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 88px;
|
||||
border-width: 1px;
|
||||
border-style: dashed;
|
||||
@@ -226,6 +228,7 @@
|
||||
|
||||
.umb-grid .cell-tools-add.-bar {
|
||||
display: block;
|
||||
width: calc(100% - 20px);
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
border: 1px dashed @ui-action-discreet-border;
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.umb-iconpicker-item a {
|
||||
.umb-iconpicker-item button {
|
||||
background: transparent;
|
||||
border: 0 none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -26,8 +28,8 @@
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.umb-iconpicker-item a:hover,
|
||||
.umb-iconpicker-item a:focus {
|
||||
.umb-iconpicker-item button:hover,
|
||||
.umb-iconpicker-item button:focus {
|
||||
background: @gray-10;
|
||||
outline: none;
|
||||
}
|
||||
@@ -39,7 +41,7 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.umb-iconpicker-item a:active {
|
||||
.umb-iconpicker-item button:active {
|
||||
background: @gray-10;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter) {
|
||||
function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter, editorState) {
|
||||
|
||||
var vm = this;
|
||||
var evts = [];
|
||||
@@ -18,6 +18,10 @@
|
||||
vm.startTour = startTour;
|
||||
vm.getTourGroupCompletedPercentage = getTourGroupCompletedPercentage;
|
||||
vm.showTourButton = showTourButton;
|
||||
|
||||
vm.showDocTypeTour = false;
|
||||
vm.docTypeTours = [];
|
||||
vm.nodeName = '';
|
||||
|
||||
function startTour(tour) {
|
||||
tourService.startTour(tour);
|
||||
@@ -58,9 +62,16 @@
|
||||
handleSectionChange();
|
||||
}));
|
||||
|
||||
evts.push(eventsService.on("editorState.changed",
|
||||
function (e, args) {
|
||||
setDocTypeTour(args.entity);
|
||||
}));
|
||||
|
||||
findHelp(vm.section, vm.tree, vm.userType, vm.userLang);
|
||||
|
||||
});
|
||||
|
||||
setDocTypeTour(editorState.getCurrent());
|
||||
|
||||
// check if a tour is running - if it is open the matching group
|
||||
var currentTour = tourService.getCurrentTour();
|
||||
@@ -84,7 +95,7 @@
|
||||
|
||||
setSectionName();
|
||||
findHelp(vm.section, vm.tree, vm.userType, vm.userLang);
|
||||
|
||||
setDocTypeTour();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -168,6 +179,26 @@
|
||||
});
|
||||
}
|
||||
|
||||
function setDocTypeTour(node) {
|
||||
vm.showDocTypeTour = false;
|
||||
vm.docTypeTours = [];
|
||||
vm.nodeName = '';
|
||||
|
||||
if (vm.section === 'content' && vm.tree === 'content') {
|
||||
|
||||
if (node) {
|
||||
tourService.getToursForDoctype(node.contentTypeAlias).then(function (data) {
|
||||
if (data && data.length > 0) {
|
||||
vm.docTypeTours = data;
|
||||
var currentVariant = _.find(node.variants, (x) => x.active);
|
||||
vm.nodeName = currentVariant.name;
|
||||
vm.showDocTypeTour = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
evts.push(eventsService.on("appState.tour.complete", function (event, tour) {
|
||||
tourService.getGroupedTours().then(function(groupedTours) {
|
||||
vm.tours = groupedTours;
|
||||
|
||||
@@ -8,12 +8,36 @@
|
||||
|
||||
<umb-drawer-content>
|
||||
|
||||
<!-- Tours -->
|
||||
<div class="umb-help-section" ng-if="vm.tours.length > 0" data-element="help-tours">
|
||||
<!-- Doctype Tours -->
|
||||
<div class="umb-help-section" ng-if="vm.showDocTypeTour" data-element="doctype-tour">
|
||||
<h5>Need help editing current item '{{vm.nodeName}}' ?</h5>
|
||||
|
||||
<h5 class="umb-help-section__title">Tours</h5>
|
||||
<div class="umb-help-list">
|
||||
|
||||
<div ng-repeat="tourGroup in vm.tours | orderBy:'groupOrder'">
|
||||
|
||||
<div ng-repeat="tour in vm.docTypeTours | orderBy:'groupOrder'">
|
||||
<div data-element="tour-{{tour.alias}}" class="umb-help-list-item">
|
||||
<div class="umb-help-list-item__content justify-between">
|
||||
<div class="flex items-center">
|
||||
<span class="umb-help-list-item__title">{{ tour.name }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<umb-button button-style="primary" size="xxs" type="button" label="Start" action="vm.startTour(tour)"></umb-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tours -->
|
||||
<div class="umb-help-section" ng-if="vm.tours.length > 0" data-element="help-tours">
|
||||
|
||||
<h5 class="umb-help-section__title">
|
||||
<localize key="help_tours">Tours</localize>
|
||||
</h5>
|
||||
|
||||
<div ng-repeat="tourGroup in vm.tours | orderBy:'groupOrder'">
|
||||
|
||||
<div class="umb-help-list">
|
||||
|
||||
@@ -25,7 +49,9 @@
|
||||
<span class="umb-help-list-item__group-title bold">
|
||||
<i ng-class="{'icon-navigation-right': !tourGroup.open, 'icon-navigation-down': tourGroup.open}" aria-hidden="true"></i>
|
||||
<span ng-if="tourGroup.group !== 'undefined'">{{tourGroup.group}}</span>
|
||||
<span ng-if="tourGroup.group === 'undefined'">Other</span>
|
||||
<span ng-if="tourGroup.group === 'undefined'">
|
||||
<localize key="general_other">Other</localize>
|
||||
</span>
|
||||
</span>
|
||||
<umb-progress-circle
|
||||
percentage="{{tourGroup.completedPercentage}}"
|
||||
@@ -51,33 +77,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show in custom help dashboard -->
|
||||
<div class="umb-help-section" data-element="help-custom-dashboard" ng-if="vm.customDashboard.length > 0">
|
||||
<div ng-repeat="dashboard in vm.customDashboard">
|
||||
<h5 ng-show="dashboard.label">{{dashboard.label}}</h5>
|
||||
<div ng-repeat="property in dashboard.properties">
|
||||
<div>
|
||||
<div ng-include="property.view"></div>
|
||||
<!-- Show in custom help dashboard -->
|
||||
<div class="umb-help-section" data-element="help-custom-dashboard" ng-if="vm.customDashboard.length > 0">
|
||||
<div ng-repeat="dashboard in vm.customDashboard">
|
||||
<h5 ng-show="dashboard.label">{{dashboard.label}}</h5>
|
||||
<div ng-repeat="property in dashboard.properties">
|
||||
<div>
|
||||
<div ng-include="property.view"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help Content -->
|
||||
<div class="umb-help-section" data-element="help-articles" ng-if="vm.topics.length > 0">
|
||||
<h5 class="umb-help-section__title">Articles</h5>
|
||||
<ul class="umb-help-list">
|
||||
<li class="umb-help-list-item" ng-repeat="topic in vm.topics track by $index">
|
||||
<a class="umb-help-list-item__content" data-element="help-article-{{topic.name}}" target="_blank" ng-href="{{topic.url}}?utm_source=core&utm_medium=help&utm_content=link&utm_campaign=tv">
|
||||
<span>
|
||||
<span class="umb-help-list-item__title">
|
||||
<span class="bold">{{topic.name}}</span>
|
||||
<span class="umb-help-list-item__open-icon icon-out"></span>
|
||||
</span>
|
||||
<span class="umb-help-list-item__description">{{topic.description}}</span>
|
||||
</span>
|
||||
<!-- Help Content -->
|
||||
<div class="umb-help-section" data-element="help-articles" ng-if="vm.topics.length > 0">
|
||||
<h5 class="umb-help-section__title">
|
||||
<localize key="general_articles">Articles</localize>
|
||||
</h5>
|
||||
<ul class="umb-help-list">
|
||||
<li class="umb-help-list-item" ng-repeat="topic in vm.topics track by $index">
|
||||
<a class="umb-help-list-item__content" data-element="help-article-{{topic.name}}" target="_blank" ng-href="{{topic.url}}?utm_source=core&utm_medium=help&utm_content=link&utm_campaign=tv">
|
||||
<span>
|
||||
<span class="umb-help-list-item__title">
|
||||
<span class="bold">{{topic.name}}</span>
|
||||
<span class="umb-help-list-item__open-icon icon-out"></span>
|
||||
</span>
|
||||
<span class="umb-help-list-item__description">{{topic.description}}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -85,7 +113,9 @@
|
||||
|
||||
<!-- Umbraco tv content -->
|
||||
<div class="umb-help-section" data-element="help-videos" ng-if="vm.hasAccessToSettings">
|
||||
<h5 class="umb-help-section__title" ng-if="vm.videos.length > 0">Videos</h5>
|
||||
<h5 class="umb-help-section__title" ng-if="vm.videos.length > 0">
|
||||
<localize key="general_videos">Videos</localize>
|
||||
</h5>
|
||||
<ul class="umb-help-list">
|
||||
<li class="umb-help-list-item" ng-repeat="video in vm.videos track by $index">
|
||||
<a class="umb-help-list-item__content" data-element="help-article-{{video.title}}" target="_blank" ng-href="{{video.link}}?utm_source=core&utm_medium=help&utm_content=link&utm_campaign=tv">
|
||||
@@ -101,7 +131,9 @@
|
||||
<div class="umb-help-section" data-element="help-links" ng-if="vm.hasAccessToSettings">
|
||||
<a data-element="help-link-umbraco-tv" class="umb-help-badge" target="_blank" href="https://umbraco.tv?utm_source=core&utm_medium=help&utm_content=link&utm_campaign=tv">
|
||||
<i class="umb-help-badge__icon icon-tv-old" aria-hidden="true"></i>
|
||||
<div class="umb-help-badge__title">Visit umbraco.tv</div>
|
||||
<div class="umb-help-badge__title">
|
||||
<localize key="help_umbracoTv">Visit umbraco.tv</localize>
|
||||
</div>
|
||||
<small>
|
||||
<localize key="help_theBestUmbracoVideoTutorials">The best Umbraco video tutorials</localize>
|
||||
</small>
|
||||
@@ -110,7 +142,9 @@
|
||||
<a data-element="help-link-our-umbraco" class="umb-help-badge" target="_blank" href="https://our.umbraco.com?utm_source=core&utm_medium=help&utm_content=link&utm_campaign=our">
|
||||
<i class="umb-help-badge__icon icon-favorite" aria-hidden="true"></i>
|
||||
|
||||
<div class="umb-help-badge__title">Visit our.umbraco.com</div>
|
||||
<div class="umb-help-badge__title">
|
||||
<localize key="help_umbracoForum">Visit our.umbraco.com</localize>
|
||||
</div>
|
||||
<small>
|
||||
<localize key="defaultdialogs_theFriendliestCommunity">The friendliest community</localize>
|
||||
</small>
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
var dialogOptions = $scope.model;
|
||||
var node = dialogOptions.currentNode;
|
||||
|
||||
$scope.model.relateToOriginal = true;
|
||||
$scope.dialogTreeApi = {};
|
||||
$scope.model.relateToOriginal = true;
|
||||
$scope.model.includeDescendants = true;
|
||||
$scope.dialogTreeApi = {};
|
||||
|
||||
vm.searchInfo = {
|
||||
searchFromId: null,
|
||||
@@ -33,14 +34,12 @@
|
||||
function onInit() {
|
||||
|
||||
var labelKeys = [
|
||||
"general_copy",
|
||||
"defaultdialogs_relateToOriginalLabel"
|
||||
"general_copy"
|
||||
];
|
||||
|
||||
localizationService.localizeMany(labelKeys).then(function (data) {
|
||||
|
||||
vm.labels.title = data[0];
|
||||
vm.labels.relateToOriginal = data[1];
|
||||
|
||||
setTitle(vm.labels.title);
|
||||
});
|
||||
@@ -132,6 +131,10 @@
|
||||
if (type === "relate") {
|
||||
$scope.model.relateToOriginal = !$scope.model.relateToOriginal;
|
||||
}
|
||||
// If the includeDescendants toggle is clicked
|
||||
if (type === "descendants") {
|
||||
$scope.model.includeDescendants = !$scope.model.includeDescendants;
|
||||
}
|
||||
}
|
||||
|
||||
onInit();
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
<umb-editor-container>
|
||||
<umb-box>
|
||||
<umb-box-content>
|
||||
<p class="abstract">
|
||||
<localize key="actions_infiniteEditorChooseWhereToCopy">Choose where to copy the selected item(s)</localize>
|
||||
</p>
|
||||
|
||||
<div ng-hide="miniListView">
|
||||
<div class="umb-control-group">
|
||||
<umb-tree-search-box
|
||||
@@ -64,6 +68,11 @@
|
||||
<umb-toggle label-position="left" checked="model.relateToOriginal" on-click="vm.onToggle('relate')"></umb-toggle>
|
||||
</umb-control-group>
|
||||
</umb-pane>
|
||||
<umb-pane>
|
||||
<umb-control-group localize="label" label="@defaultdialogs_includeDescendants">
|
||||
<umb-toggle label-position="left" checked="model.includeDescendants" on-click="vm.onToggle('descendants')"></umb-toggle>
|
||||
</umb-control-group>
|
||||
</umb-pane>
|
||||
</umb-box-content>
|
||||
</umb-box>
|
||||
</umb-editor-container>
|
||||
@@ -82,7 +91,8 @@
|
||||
button-style="success"
|
||||
label-key="general_submit"
|
||||
state="vm.saveButtonState"
|
||||
action="vm.submit(model)">
|
||||
action="vm.submit(model)"
|
||||
disabled="!model.target">
|
||||
</umb-button>
|
||||
</umb-editor-footer-content-right>
|
||||
</umb-editor-footer>
|
||||
|
||||
+4
-3
@@ -46,9 +46,10 @@
|
||||
<div class="umb-control-group" ng-show="!vm.loading && filtered.length > 0 ">
|
||||
<ul class="umb-iconpicker" ng-class="vm.color.value" ng-show="vm.icons">
|
||||
<li class="umb-iconpicker-item" ng-class="{'-selected': icon == model.icon}" ng-repeat="icon in filtered = (vm.icons | filter: searchTerm) track by $id(icon)">
|
||||
<a href="#" title="{{icon}}" ng-click="vm.selectIcon(icon, vm.color.value)" prevent-default>
|
||||
<i class="{{icon}} large"></i>
|
||||
</a>
|
||||
<button type="button" title="{{icon}}" ng-click="vm.selectIcon(icon, vm.color.value)" prevent-default>
|
||||
<i class="{{icon}} large" aria-hidden="true"></i>
|
||||
<span class="sr-only"><localize key="buttons_select">Select</localize> {{icon}}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController",
|
||||
};
|
||||
|
||||
$scope.showTarget = $scope.model.hideTarget !== true;
|
||||
$scope.showAnchor = $scope.model.hideAnchor !== true;
|
||||
|
||||
// this ensures that we only sync the tree once and only when it's ready
|
||||
var oneTimeTreeSync = {
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@
|
||||
<umb-box>
|
||||
<umb-box-content>
|
||||
|
||||
<div class="flex">
|
||||
<div ng-class="{'flex': showAnchor}">
|
||||
|
||||
<umb-control-group label="@defaultdialogs_urlLinkPicker" class="umb-linkpicker__url">
|
||||
<input type="text"
|
||||
@@ -27,7 +27,7 @@
|
||||
ng-disabled="model.target.id || model.target.udi" />
|
||||
</umb-control-group>
|
||||
|
||||
<umb-control-group label="@defaultdialogs_anchorLinkPicker" class="umb-linkpicker__anchor">
|
||||
<umb-control-group label="@defaultdialogs_anchorLinkPicker" class="umb-linkpicker__anchor" ng-if="showAnchor">
|
||||
<input type="text"
|
||||
list="anchors"
|
||||
localize="placeholder"
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
<umb-editor-container>
|
||||
<umb-box>
|
||||
<umb-box-content>
|
||||
<p class="abstract" ng-hide="success">
|
||||
<localize key="actions_infiniteEditorChooseWhereToMove">Choose where to move the selected item(s)</localize>
|
||||
</p>
|
||||
|
||||
<div ng-hide="miniListView">
|
||||
<div class="umb-control-group">
|
||||
@@ -75,7 +78,8 @@
|
||||
button-style="success"
|
||||
label-key="general_submit"
|
||||
state="vm.saveButtonState"
|
||||
action="vm.submit(model)">
|
||||
action="vm.submit(model)"
|
||||
disabled="!model.target">
|
||||
</umb-button>
|
||||
</umb-editor-footer-content-right>
|
||||
</umb-editor-footer>
|
||||
|
||||
@@ -19,14 +19,13 @@
|
||||
</div>
|
||||
|
||||
<ul class="umb-card-grid" ng-class="{'-three-in-row': model.availableItems.length < 7, '-four-in-row': model.availableItems.length >= 7}">
|
||||
<li ng-repeat="pasteItem in model.pasteItems | filter:searchTerm"
|
||||
ng-click="model.clickPasteItem(pasteItem)">
|
||||
<a class="umb-card-grid-item" href="" title="{{ pasteItem.name }}">
|
||||
<li ng-repeat="pasteItem in model.pasteItems | filter:searchTerm">
|
||||
<button class="umb-card-grid-item btn-reset" ng-click="model.clickPasteItem(pasteItem)">
|
||||
<span>
|
||||
<i class="{{ pasteItem.icon }}"></i>
|
||||
{{ pasteItem.name | truncate:true:36 }}
|
||||
</span>
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -35,15 +34,13 @@
|
||||
</div>
|
||||
|
||||
<ul class="umb-card-grid" ng-class="{'-three-in-row': model.availableItems.length < 7, '-four-in-row': model.availableItems.length >= 7}">
|
||||
<li ng-repeat="availableItem in model.availableItems | compareArrays:model.selectedItems:'alias' | orderBy:model.orderBy | filter:searchTerm"
|
||||
ng-click="selectItem(availableItem)">
|
||||
<a class="umb-card-grid-item" href="" title="{{ availableItem.name }}">
|
||||
<li ng-repeat="availableItem in model.availableItems | compareArrays:model.selectedItems:'alias' | orderBy:model.orderBy | filter:searchTerm">
|
||||
<button class="umb-card-grid-item btn-reset" ng-click="selectItem(availableItem)">
|
||||
<span>
|
||||
<i class="{{ availableItem.icon }}"></i>
|
||||
{{ availableItem.name }}
|
||||
</span>
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,21 @@
|
||||
<div class="umb-search" on-outside-click="vm.closeSearch()" ng-keydown="vm.handleKeyDown($event)">
|
||||
|
||||
<div class="flex items-center">
|
||||
<i class="umb-search-input-icon icon-search" ng-click="vm.focusSearch()"></i>
|
||||
<input
|
||||
class="umb-search-input"
|
||||
type="text"
|
||||
<label for="app-search" class="umb-search__label">
|
||||
<i class="umb-search-input-icon icon-search" aria-hidden="true"></i>
|
||||
<span class="sr-only">
|
||||
<localize key="general_search">Search...</localize>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
class="umb-search-input"
|
||||
type="text"
|
||||
ng-model="vm.searchQuery"
|
||||
ng-model-options="{ debounce: 200 }"
|
||||
ng-change="vm.search(vm.searchQuery)"
|
||||
localize="placeholder"
|
||||
placeholder="@placeholders_search"
|
||||
id="app-search"
|
||||
focus-when="{{vm.searchHasFocus}}" />
|
||||
<button ng-show="vm.searchQuery.length > 0" tabindex="-1" class="umb-search-input-clear umb-animated" ng-click="vm.clearSearch()">
|
||||
<localize key="general_clear">Clear</localize>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="umb-child-selector__children-container">
|
||||
<div class="umb-child-selector__children-container" ui-sortable="sortableOptions" ng-model="selectedChildren">
|
||||
|
||||
<div class="umb-child-selector__child" ng-repeat="selectedChild in selectedChildren">
|
||||
<div class="umb-child-selector__child-description">
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
'-left': direction === 'left'}"
|
||||
on-outside-click="clickCancel()">
|
||||
|
||||
<a class="umb_confirm-action__overlay-action -confirm" href="" ng-click="clickConfirm()" localize="title" title="@buttons_confirmActionConfirm">
|
||||
<button class="umb_confirm-action__overlay-action -confirm btn-reset" ng-click="clickConfirm()" localize="title" title="@buttons_confirmActionConfirm" type="button">
|
||||
<i class="icon-check"></i>
|
||||
</a>
|
||||
</button>
|
||||
|
||||
<a class="umb_confirm-action__overlay-action -cancel" href="" ng-click="clickCancel()" localize="title" title="@buttons_confirmActionCancel">
|
||||
<button class="umb_confirm-action__overlay-action -cancel btn-reset" ng-click="clickCancel()" localize="title" title="@buttons_confirmActionCancel" type="button">
|
||||
<i class="icon-delete"></i>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<ul class="umb-actions umb-actions-child" ng-if="selectContentType && allowedTypes.length > 0" aria-labelledby="selectContentType">
|
||||
|
||||
<li class="umb-action" data-element="action-create-{{docType.alias}}" ng-repeat="docType in allowedTypes | orderBy:'name':false">
|
||||
<li class="umb-action" data-element="action-create-{{docType.alias}}" ng-repeat="docType in allowedTypes">
|
||||
<button class="umb-action-link umb-outline btn-reset" ng-click="createOrSelectBlueprintIfAny(docType)" umb-auto-focus ng-if="$index === 0">
|
||||
<i class="large icon {{docType.icon}}"></i>
|
||||
<span class="menu-label">
|
||||
|
||||
@@ -2,20 +2,25 @@
|
||||
<div class="installer-wrapper">
|
||||
|
||||
<div class="installer-intro">
|
||||
<i class="icon icon-umb-contour"></i>
|
||||
<h3>Umbraco Forms</h3>
|
||||
<i class="icon icon-umb-contour" aria-hidden="true"></i>
|
||||
<h3>
|
||||
<localize key="formsDashboard_formsHeadline">Umbraco Forms</localize>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<!-- STEP one -->
|
||||
<div class="step-one" ng-hide="vm.state">
|
||||
|
||||
<p class="step-text">Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it!</p>
|
||||
<p class="step-text">
|
||||
<localize key="formsDashboard_formsDescription">Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it!</localize>
|
||||
</p>
|
||||
|
||||
<umb-button
|
||||
class="forms-install-button"
|
||||
type="button"
|
||||
button-style="success"
|
||||
label="Install"
|
||||
label-key="general_install"
|
||||
action="vm.installForms()">
|
||||
</umb-button>
|
||||
|
||||
@@ -23,7 +28,7 @@
|
||||
|
||||
<!-- STEP two -->
|
||||
<div class="step-two" ng-if="vm.state">
|
||||
<p id="installer" class="step-text">Installing...</p>
|
||||
<p id="installer" class="step-text"><localize key="general_installing">Installing</localize>...</p>
|
||||
|
||||
<div class="installing">
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @description
|
||||
* The controller for the info view of the datatype editor
|
||||
*/
|
||||
function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsService, $timeout) {
|
||||
function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsService, $timeout, editorService) {
|
||||
|
||||
var vm = this;
|
||||
var evts = [];
|
||||
@@ -17,6 +17,9 @@ function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsSe
|
||||
|
||||
vm.view = {};
|
||||
vm.view.loading = true;
|
||||
vm.openDocumentType = openDocumentType;
|
||||
vm.openMediaType = openMediaType;
|
||||
vm.openMemberType = openMemberType;
|
||||
|
||||
/** Loads in the data type references one time */
|
||||
function loadRelations() {
|
||||
@@ -31,6 +34,57 @@ function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsSe
|
||||
}
|
||||
}
|
||||
|
||||
function openDocumentType(id, event) {
|
||||
open(id, event, "documentType");
|
||||
}
|
||||
|
||||
function openMediaType(id, event) {
|
||||
open(id, event, "mediaType");
|
||||
}
|
||||
|
||||
function openMemberType(id, event) {
|
||||
open(id, event, "memberType");
|
||||
}
|
||||
|
||||
function open(id, event, type) {
|
||||
// targeting a new tab/window?
|
||||
if (event.ctrlKey ||
|
||||
event.shiftKey ||
|
||||
event.metaKey || // apple
|
||||
(event.button && event.button === 1) // middle click, >IE9 + everyone else
|
||||
) {
|
||||
// yes, let the link open itself
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
const editor = {
|
||||
id: id,
|
||||
submit: function (model) {
|
||||
editorService.close();
|
||||
vm.view.loading = true;
|
||||
referencesLoaded = false;
|
||||
loadRelations();
|
||||
},
|
||||
close: function () {
|
||||
editorService.close();
|
||||
}
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case "documentType":
|
||||
editorService.documentTypeEditor(editor);
|
||||
break;
|
||||
case "mediaType":
|
||||
editorService.mediaTypeEditor(editor);
|
||||
break;
|
||||
case "memberType":
|
||||
editorService.memberTypeEditor(editor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// load data type references when the references tab is activated
|
||||
evts.push(eventsService.on("app.tabChange", function (event, args) {
|
||||
$timeout(function () {
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="umb-table-cell umb-table__name"><span>{{::reference.name}}</span></div>
|
||||
<div class="umb-table-cell"><span title="{{::reference.alias}}">{{::reference.alias}}</span></div>
|
||||
<div class="umb-table-cell --noOverflow"><span>{{::reference.properties | umbCmsJoinArray:', ':'name'}}</span></div>
|
||||
<div class="umb-table-cell umb-table-cell--nano"><a href="#/settings/documentTypes/edit/{{::reference.id}}"><localize key="general_open">Open</localize></a></div>
|
||||
<div class="umb-table-cell umb-table-cell--nano"><a href="#/settings/documentTypes/edit/{{::reference.id}}" ng-click="vm.openDocumentType(reference.id, $event)"><localize key="general_open">Open</localize></a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,7 +73,7 @@
|
||||
<div class="umb-table-cell umb-table__name"><span>{{::reference.name}}</span></div>
|
||||
<div class="umb-table-cell"><span title="{{::reference.alias}}">{{::reference.alias}}</span></div>
|
||||
<div class="umb-table-cell --noOverflow"><span>{{::reference.properties | umbCmsJoinArray:', ':'name'}}</span></div>
|
||||
<div class="umb-table-cell umb-table-cell--nano"><a href="#/settings/mediaTypes/edit/{{::reference.id}}"><localize key="general_open">Open</localize></a></div>
|
||||
<div class="umb-table-cell umb-table-cell--nano"><a href="#/settings/mediaTypes/edit/{{::reference.id}}" ng-click="vm.openMediaType(reference.id, $event)"><localize key="general_open">Open</localize></a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,7 +104,7 @@
|
||||
<div class="umb-table-cell umb-table__name"><span>{{::reference.name}}</span></div>
|
||||
<div class="umb-table-cell"><span title="{{::reference.alias}}">{{::reference.alias}}</span></div>
|
||||
<div class="umb-table-cell --noOverflow"><span>{{::reference.properties | umbCmsJoinArray:', ':'name'}}</span></div>
|
||||
<div class="umb-table-cell umb-table-cell--nano"><a href="#/settings/memberTypes/edit/{{::reference.id}}"><localize key="general_open">Open</localize></a></div>
|
||||
<div class="umb-table-cell umb-table-cell--nano"><a href="#/settings/memberTypes/edit/{{::reference.id}}" ng-click="vm.openMemberType(reference.id, $event)"><localize key="general_open">Open</localize></a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+18
-2
@@ -9,7 +9,7 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
function PermissionsController($scope, contentTypeResource, iconHelper, contentTypeHelper, localizationService, overlayService) {
|
||||
function PermissionsController($scope, $timeout, contentTypeResource, iconHelper, contentTypeHelper, localizationService, overlayService) {
|
||||
|
||||
/* ----------- SCOPE VARIABLES ----------- */
|
||||
|
||||
@@ -23,8 +23,10 @@
|
||||
|
||||
vm.addChild = addChild;
|
||||
vm.removeChild = removeChild;
|
||||
vm.sortChildren = sortChildren;
|
||||
vm.toggleAllowAsRoot = toggleAllowAsRoot;
|
||||
vm.toggleAllowCultureVariants = toggleAllowCultureVariants;
|
||||
vm.canToggleIsElement = false;
|
||||
vm.toggleIsElement = toggleIsElement;
|
||||
|
||||
/* ---------- INIT ---------- */
|
||||
@@ -48,9 +50,16 @@
|
||||
if($scope.model.id === 0) {
|
||||
contentTypeHelper.insertChildNodePlaceholder(vm.contentTypes, $scope.model.name, $scope.model.icon, $scope.model.id);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Can only switch to an element type if there are no content nodes already created from the type.
|
||||
if ($scope.model.id > 0 && !$scope.model.isElement ) {
|
||||
contentTypeResource.hasContentNodes($scope.model.id).then(function (result) {
|
||||
vm.canToggleIsElement = !result;
|
||||
});
|
||||
} else {
|
||||
vm.canToggleIsElement = true;
|
||||
}
|
||||
}
|
||||
|
||||
function addChild($event) {
|
||||
@@ -84,6 +93,13 @@
|
||||
$scope.model.allowedContentTypes.splice(selectedChildIndex, 1);
|
||||
}
|
||||
|
||||
function sortChildren() {
|
||||
// we need to wait until the next digest cycle for vm.selectedChildren to be updated
|
||||
$timeout(function () {
|
||||
$scope.model.allowedContentTypes = _.pluck(vm.selectedChildren, "id");
|
||||
});
|
||||
}
|
||||
|
||||
// note: "safe toggling" here ie handling cases where the value is undefined, etc
|
||||
|
||||
function toggleAllowAsRoot() {
|
||||
|
||||
+4
-1
@@ -33,7 +33,8 @@
|
||||
parent-icon="model.icon"
|
||||
parent-id="model.id"
|
||||
on-add="vm.addChild"
|
||||
on-remove="vm.removeChild">
|
||||
on-remove="vm.removeChild"
|
||||
on-sort="vm.sortChildren">
|
||||
</umb-child-selector>
|
||||
</div>
|
||||
|
||||
@@ -61,11 +62,13 @@
|
||||
<div class="sub-view-column-left">
|
||||
<h5><localize key="contentTypeEditor_elementHeading" /></h5>
|
||||
<small><localize key="contentTypeEditor_elementDescription" /></small>
|
||||
<small ng-if="!vm.canToggleIsElement"><br/><localize key="contentTypeEditor_elementCannotToggle" /></small>
|
||||
</div>
|
||||
|
||||
<div class="sub-view-column-right">
|
||||
<umb-toggle data-element="permissions-is-element"
|
||||
checked="model.isElement"
|
||||
disabled="!vm.canToggleIsElement"
|
||||
on-click="vm.toggleIsElement()">
|
||||
</umb-toggle>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<umb-editor-header
|
||||
name="vm.contentType.name"
|
||||
alias="vm.contentType.alias"
|
||||
alias-locked="vm.contentType.isSystemMediaType"
|
||||
key="vm.contentType.key"
|
||||
description="vm.contentType.description"
|
||||
navigation="vm.page.navigation"
|
||||
|
||||
+9
-1
@@ -1,7 +1,7 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
function PermissionsController($scope, mediaTypeResource, iconHelper, contentTypeHelper, localizationService, overlayService) {
|
||||
function PermissionsController($scope, $timeout, mediaTypeResource, iconHelper, contentTypeHelper, localizationService, overlayService) {
|
||||
|
||||
/* ----------- SCOPE VARIABLES ----------- */
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
vm.addChild = addChild;
|
||||
vm.removeChild = removeChild;
|
||||
vm.sortChildren = sortChildren;
|
||||
vm.toggle = toggle;
|
||||
|
||||
/* ---------- INIT ---------- */
|
||||
@@ -71,6 +72,13 @@
|
||||
$scope.model.allowedContentTypes.splice(selectedChildIndex, 1);
|
||||
}
|
||||
|
||||
function sortChildren() {
|
||||
// we need to wait until the next digest cycle for vm.selectedChildren to be updated
|
||||
$timeout(function () {
|
||||
$scope.model.allowedContentTypes = _.pluck(vm.selectedChildren, "id");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the $scope.model.allowAsRoot value to either true or false
|
||||
*/
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
parent-icon="model.icon"
|
||||
parent-id="model.id"
|
||||
on-add="vm.addChild"
|
||||
on-remove="vm.removeChild">
|
||||
on-remove="vm.removeChild"
|
||||
on-sort="vm.sortChildren">
|
||||
</umb-child-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,8 +13,13 @@
|
||||
|
||||
var evts = [];
|
||||
var vm = this;
|
||||
var infiniteMode = $scope.model && $scope.model.infiniteMode;
|
||||
var memberTypeId = infiniteMode ? $scope.model.id : $routeParams.id;
|
||||
var create = infiniteMode ? $scope.model.create : $routeParams.create;
|
||||
|
||||
vm.save = save;
|
||||
vm.close = close;
|
||||
|
||||
vm.editorfor = "visuallyHiddenTexts_newMember";
|
||||
vm.header = {};
|
||||
vm.header.editorfor = "content_membergroup";
|
||||
@@ -25,6 +30,7 @@
|
||||
vm.page.loading = false;
|
||||
vm.page.saveButtonState = "init";
|
||||
vm.labels = {};
|
||||
vm.saveButtonKey = infiniteMode ? "buttons_saveAndClose" : "buttons_save";
|
||||
|
||||
var labelKeys = [
|
||||
"general_design",
|
||||
@@ -86,7 +92,7 @@
|
||||
vm.page.defaultButton = {
|
||||
hotKey: "ctrl+s",
|
||||
hotKeyWhenHidden: true,
|
||||
labelKey: "buttons_save",
|
||||
labelKey: vm.saveButtonKey,
|
||||
letter: "S",
|
||||
type: "submit",
|
||||
handler: function () { vm.save(); }
|
||||
@@ -94,7 +100,7 @@
|
||||
vm.page.subButtons = [{
|
||||
hotKey: "ctrl+g",
|
||||
hotKeyWhenHidden: true,
|
||||
labelKey: "buttons_saveAndGenerateModels",
|
||||
labelKey: infiniteMode ? "buttons_generateModelsAndClose" : "buttons_saveAndGenerateModels",
|
||||
letter: "G",
|
||||
handler: function () {
|
||||
|
||||
@@ -147,12 +153,12 @@
|
||||
}
|
||||
});
|
||||
|
||||
if ($routeParams.create) {
|
||||
if (create) {
|
||||
|
||||
vm.page.loading = true;
|
||||
|
||||
//we are creating so get an empty data type item
|
||||
memberTypeResource.getScaffold($routeParams.id)
|
||||
memberTypeResource.getScaffold(memberTypeId)
|
||||
.then(function (dt) {
|
||||
init(dt);
|
||||
|
||||
@@ -163,10 +169,12 @@
|
||||
|
||||
vm.page.loading = true;
|
||||
|
||||
memberTypeResource.getById($routeParams.id).then(function (dt) {
|
||||
memberTypeResource.getById(memberTypeId).then(function (dt) {
|
||||
init(dt);
|
||||
|
||||
syncTreeNode(vm.contentType, dt.path, true);
|
||||
if(!infiniteMode) {
|
||||
syncTreeNode(vm.contentType, dt.path, true);
|
||||
}
|
||||
|
||||
vm.page.loading = false;
|
||||
});
|
||||
@@ -219,10 +227,16 @@
|
||||
}
|
||||
}).then(function (data) {
|
||||
//success
|
||||
syncTreeNode(vm.contentType, data.path);
|
||||
if(!infiniteMode) {
|
||||
syncTreeNode(vm.contentType, data.path);
|
||||
}
|
||||
|
||||
vm.page.saveButtonState = "success";
|
||||
|
||||
if(infiniteMode && $scope.model.submit) {
|
||||
$scope.model.submit();
|
||||
}
|
||||
|
||||
deferred.resolve(data);
|
||||
}, function (err) {
|
||||
//error
|
||||
@@ -307,6 +321,12 @@
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function close() {
|
||||
if(infiniteMode && $scope.model.close) {
|
||||
$scope.model.close();
|
||||
}
|
||||
}
|
||||
|
||||
evts.push(eventsService.on("editors.groupsBuilder.changed", function(name, args) {
|
||||
angularHelper.getCurrentForm($scope).$setDirty();
|
||||
|
||||
@@ -40,6 +40,14 @@
|
||||
|
||||
<umb-editor-footer-content-right>
|
||||
|
||||
<umb-button ng-if="model.infiniteMode"
|
||||
type="button"
|
||||
button-style="link"
|
||||
label-key="general_close"
|
||||
shortcut="esc"
|
||||
action="vm.close()">
|
||||
</umb-button>
|
||||
|
||||
<umb-button
|
||||
ng-if="!vm.page.modelsBuilder"
|
||||
type="button"
|
||||
@@ -47,8 +55,7 @@
|
||||
state="vm.page.saveButtonState"
|
||||
button-style="success"
|
||||
shortcut="ctrl+s"
|
||||
label="Save"
|
||||
label-key="buttons_save">
|
||||
label-key="{{vm.saveButtonKey}}">
|
||||
</umb-button>
|
||||
|
||||
<umb-button-group
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@
|
||||
<span class="sr-only">...</span>
|
||||
</button>
|
||||
|
||||
<div class="umb-contentpicker__min-max-help" ng-if="model.config.multiPicker === true">
|
||||
<div class="umb-contentpicker__min-max-help" ng-if="model.config.multiPicker === true && (model.config.maxNumber > 1 || model.config.minNumber > 0)">
|
||||
|
||||
<!-- Both min and max items -->
|
||||
<span ng-if="model.config.minNumber && model.config.maxNumber && model.config.minNumber !== model.config.maxNumber">
|
||||
|
||||
+22
-10
@@ -85,13 +85,27 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM
|
||||
};
|
||||
|
||||
$scope.datePickerChange = function(date) {
|
||||
setDate(date);
|
||||
const momentDate = moment(date);
|
||||
setDate(momentDate);
|
||||
setDatePickerVal();
|
||||
};
|
||||
|
||||
$scope.inputChanged = function() {
|
||||
setDate($scope.model.datetimePickerValue);
|
||||
setDatePickerVal();
|
||||
$scope.inputChanged = function () {
|
||||
if ($scope.model.datetimePickerValue === "" && $scope.hasDatetimePickerValue) {
|
||||
// $scope.hasDatetimePickerValue indicates that we had a value before the input was changed,
|
||||
// but now the input is empty.
|
||||
$scope.clearDate();
|
||||
} else if ($scope.model.datetimePickerValue) {
|
||||
var momentDate = moment($scope.model.datetimePickerValue, $scope.model.config.format, true);
|
||||
if (!momentDate || !momentDate.isValid()) {
|
||||
momentDate = moment(new Date($scope.model.datetimePickerValue));
|
||||
}
|
||||
if (momentDate && momentDate.isValid()) {
|
||||
setDate(momentDate);
|
||||
}
|
||||
setDatePickerVal();
|
||||
flatPickr.setDate($scope.model.value, false);
|
||||
}
|
||||
}
|
||||
|
||||
//here we declare a special method which will be called whenever the value has changed from the server
|
||||
@@ -103,15 +117,14 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM
|
||||
var newDate = moment(newVal);
|
||||
|
||||
if (newDate.isAfter(minDate)) {
|
||||
setDate(newVal);
|
||||
setDate(newDate);
|
||||
} else {
|
||||
$scope.clearDate();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function setDate(date) {
|
||||
const momentDate = moment(date);
|
||||
function setDate(momentDate) {
|
||||
angularHelper.safeApply($scope, function() {
|
||||
// when a date is changed, update the model
|
||||
if (momentDate && momentDate.isValid()) {
|
||||
@@ -123,12 +136,11 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM
|
||||
$scope.hasDatetimePickerValue = false;
|
||||
$scope.model.datetimePickerValue = null;
|
||||
}
|
||||
updateModelValue(date);
|
||||
updateModelValue(momentDate);
|
||||
});
|
||||
}
|
||||
|
||||
function updateModelValue(date) {
|
||||
const momentDate = moment(date);
|
||||
function updateModelValue(momentDate) {
|
||||
if ($scope.hasDatetimePickerValue) {
|
||||
if ($scope.model.config.pickTime) {
|
||||
//check if we are supposed to offset the time
|
||||
|
||||
@@ -402,6 +402,15 @@ angular.module("umbraco")
|
||||
|
||||
eventsService.emit("grid.rowAdded", { scope: $scope, element: $element, row: row });
|
||||
|
||||
// TODO: find a nicer way to do this without relying on setTimeout
|
||||
setTimeout(function () {
|
||||
var newRowEl = $element.find("[data-rowid='" + row.$uniqueId + "']");
|
||||
|
||||
if(newRowEl !== null) {
|
||||
newRowEl.focus();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
};
|
||||
|
||||
$scope.removeRow = function (section, $index) {
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
</div>
|
||||
|
||||
<div class="cell-tools-remove row-tool">
|
||||
<i class="icon-trash" ng-click="togglePrompt(row)" localize="title" title="@delete"></i>
|
||||
<button class="icon-trash btn-reset" ng-click="togglePrompt(row)" type="button"></button>
|
||||
<umb-confirm-action
|
||||
ng-if="row.deletePrompt"
|
||||
direction="left"
|
||||
@@ -161,12 +161,12 @@
|
||||
<div class="umb-cell-inner" ui-sortable="sortableOptionsCell" umb-grid-hack-scope ng-model="area.controls">
|
||||
|
||||
<!-- Control placeholder -->
|
||||
<div class="umb-cell-placeholder" ng-if="area.controls.length === 0" ng-click="openEditorOverlay($event, area, 0, area.$uniqueId);">
|
||||
<button class="umb-cell-placeholder btn-reset" type="button" ng-if="area.controls.length === 0" ng-click="openEditorOverlay($event, area, 0, area.$uniqueId);">
|
||||
<div class="cell-tools-add -center">
|
||||
<localize ng-if="!sortMode" key="grid_addElement" />
|
||||
<localize ng-if="sortMode" key="grid_dropElement" />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- for each control in areas -->
|
||||
<div class="umb-control"
|
||||
@@ -196,11 +196,11 @@
|
||||
<div class="umb-tools" ng-if="control === active">
|
||||
|
||||
<div class="umb-control-tool" ng-if="control.editor.config.settings">
|
||||
<i class="umb-control-tool-icon icon-settings" ng-click="editGridItemSettings(control, 'control')"></i>
|
||||
<button class="umb-control-tool-icon icon-settings btn-reset" ng-click="editGridItemSettings(control, 'control')" type="button"></button>
|
||||
</div>
|
||||
|
||||
<div class="umb-control-tool">
|
||||
<i class="umb-control-tool-icon icon-trash" ng-click="togglePrompt(control)" localize="title" title="@delete"></i>
|
||||
<button class="umb-control-tool-icon icon-trash btn-reset" ng-click="togglePrompt(control)" type="button"></button>
|
||||
<umb-confirm-action ng-if="control.deletePrompt"
|
||||
direction="left"
|
||||
on-confirm="removeControl(area, $index)"
|
||||
@@ -226,7 +226,7 @@
|
||||
|
||||
<!-- if area is empty tools -->
|
||||
<div class="umb-grid-add-more-content" ng-if="area.controls.length > 0 && !sortMode && (area.maxItems == undefined || area.maxItems == '' || area.maxItems == 0 || area.maxItems > area.controls.length)">
|
||||
<div class="cell-tools-add -bar newbtn" ng-click="openEditorOverlay($event, area, 0, area.$uniqueId);"><localize key="grid_addElement" /></div>
|
||||
<button class="cell-tools-add -bar newbtn btn-reset" type="button" ng-click="openEditorOverlay($event, area, 0, area.$uniqueId);"><localize key="grid_addElement" /></button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -249,14 +249,16 @@
|
||||
|
||||
<div class="umb-add-row" ng-if="!sortMode">
|
||||
|
||||
<a href=""
|
||||
class="iconBox"
|
||||
<button
|
||||
class="iconBox btn-reset"
|
||||
ng-click="toggleAddRow()"
|
||||
ng-if="!showRowConfigurations">
|
||||
|
||||
<i class=" icon icon-add" title="@general_add" localize="title"></i>
|
||||
|
||||
</a>
|
||||
<i class="icon icon-add" ></i>
|
||||
<span class="sr-only">
|
||||
<localize key="visuallyHiddenTexts_addNewRow">Add new role</localize>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -264,10 +266,11 @@
|
||||
|
||||
<p ng-hide="section.rows.length > 0"><strong><localize key="grid_addRows" /></strong></p>
|
||||
|
||||
<div class="preview-rows columns"
|
||||
<button class="preview-rows columns btn-reset"
|
||||
ng-repeat="layout in section.$allowedLayouts"
|
||||
ng-show="layout.areas.length > 0"
|
||||
ng-click="addRow(section, layout)">
|
||||
ng-click="addRow(section, layout)"
|
||||
type="button">
|
||||
|
||||
<div class="preview-row">
|
||||
|
||||
@@ -285,7 +288,7 @@
|
||||
|
||||
<small>{{layout.label || layout.name}}</small>
|
||||
|
||||
</div> <!-- .templates-preview-rows -->
|
||||
</button> <!-- .templates-preview-rows -->
|
||||
|
||||
</div> <!-- .templates-preview -->
|
||||
<!-- column tools end -->
|
||||
|
||||
@@ -620,7 +620,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
|
||||
currentNode: $scope.contentId,
|
||||
submit: function (model) {
|
||||
if (model.target) {
|
||||
performCopy(model.target, model.relateToOriginal);
|
||||
performCopy(model.target, model.relateToOriginal, model.includeDescendants);
|
||||
}
|
||||
editorService.close();
|
||||
},
|
||||
@@ -631,9 +631,9 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
|
||||
editorService.copy(copyEditor);
|
||||
};
|
||||
|
||||
function performCopy(target, relateToOriginal) {
|
||||
function performCopy(target, relateToOriginal, includeDescendants) {
|
||||
applySelected(
|
||||
function (selected, index) { return contentResource.copy({ parentId: target.id, id: getIdCallback(selected[index]), relateToOriginal: relateToOriginal }); },
|
||||
function (selected, index) { return contentResource.copy({ parentId: target.id, id: getIdCallback(selected[index]), relateToOriginal: relateToOriginal, recursive: includeDescendants }); },
|
||||
function (count, total) {
|
||||
var key = (total === 1 ? "bulk_copiedItemOfItem" : "bulk_copiedItemOfItems");
|
||||
return localizationService.localize(key, [count, total]);
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
|
||||
<!-- Renders when it's only possible to create one specific document type for which a blueprint exits-->
|
||||
<div class="btn-group" ng-show="createAllowedButtonSingleWithBlueprints" deep-blur="leaveDropdown()">
|
||||
<button type="button" class="btn btn-outline umb-outline dropdown-toggle" aria-expanded="{{page.createDropdownOpen}}" data-toggle="dropdown" ng-click="toggleDropdown()" prevent-default>
|
||||
<button type="button" class="btn btn-white dropdown-toggle" aria-expanded="{{page.createDropdownOpen}}" ng-click="toggleDropdown()" prevent-default>
|
||||
<span>
|
||||
<localize key="actions_create">Create</localize> {{listViewAllowedTypes[0].name}}
|
||||
</span>
|
||||
<span class="caret"></span>
|
||||
<span class="caret" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
<!-- Renders when it's possible to create multiple document types and blueprints for one or more of the document types-->
|
||||
<div class="btn-group" ng-show="createAllowedButtonMultiWithBlueprints" deep-blur="leaveDropdown()">
|
||||
<button type="button" class="btn btn-outline umb-outline dropdown-toggle" aria-expanded="{{page.createDropdownOpen === undefined ? false : page.createDropdownOpen}}" data-toggle="dropdown" ng-click="toggleDropdown()">
|
||||
<button type="button" class="btn btn-white dropdown-toggle" aria-expanded="{{page.createDropdownOpen === undefined ? false : page.createDropdownOpen}}" ng-click="toggleDropdown()">
|
||||
<localize key="actions_create">Create</localize>
|
||||
<span class="caret" aria-hidden="true"></span>
|
||||
</button>
|
||||
@@ -70,21 +70,21 @@
|
||||
<ul class="umb-actions umb-actions-child">
|
||||
|
||||
<li ng-repeat="blueprint in docType.blueprints track by blueprint.id | orderBy:'name':false">
|
||||
<a ng-click="createFromBlueprint(blueprint.id)">
|
||||
<i class="large {{docType.icon}}"></i>
|
||||
<button type="button" ng-click="createFromBlueprint(blueprint.id)">
|
||||
<i class="large {{docType.icon}}" aria-hidden="true"></i>
|
||||
<span class="menu-label">
|
||||
{{blueprint.name}}
|
||||
</span>
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li class="sep" ng-show="allowBlank">
|
||||
<a ng-click="createBlank(docType)">
|
||||
<i class="large {{docType.icon}}"></i>
|
||||
<button type="button" ng-click="createBlank(docType)">
|
||||
<i class="large {{docType.icon}}" aria-hidden="true"></i>
|
||||
<span class="menu-label">
|
||||
<localize key="blueprints_blankBlueprint">Blank</localize>
|
||||
</span>
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
+9
-3
@@ -1,10 +1,16 @@
|
||||
<div>
|
||||
<ng-form name="listViewOrderDirectionForm" class="flex">
|
||||
<umb-radiobutton name="orderDirection" value="asc" model="model.value" text="Ascending [a-z]" required></umb-radiobutton>
|
||||
<umb-radiobutton name="orderDirection" value="desc" model="model.value" text="Descending [z-a]" required></umb-radiobutton>
|
||||
<ul class="inline">
|
||||
<li class="-no-padding-left">
|
||||
<umb-radiobutton name="orderDirection" value="asc" model="model.value" text="Ascending [a-z]" required></umb-radiobutton>
|
||||
</li>
|
||||
<li>
|
||||
<umb-radiobutton name="orderDirection" value="desc" model="model.value" text="Descending [z-a]" required></umb-radiobutton>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<span ng-messages="listViewOrderDirectionForm.orderDirection.$error" show-validation-on-submit>
|
||||
<span class="help-inline" ng-message="required">Required</span>
|
||||
<span class="help-inline" ng-message="required"><localize key="general_required">Required</localize></span>
|
||||
</span>
|
||||
</ng-form>
|
||||
</div>
|
||||
|
||||
+20
@@ -27,6 +27,20 @@ function MarkdownEditorController($scope, $element, assetsService, editorService
|
||||
editorService.mediaPicker(mediaPicker);
|
||||
}
|
||||
|
||||
function openLinkPicker(callback) {
|
||||
var linkPicker = {
|
||||
hideTarget: true,
|
||||
submit: function(model) {
|
||||
callback(model.target.url, model.target.name);
|
||||
editorService.close();
|
||||
},
|
||||
close: function() {
|
||||
editorService.close();
|
||||
}
|
||||
};
|
||||
editorService.linkPicker(linkPicker);
|
||||
}
|
||||
|
||||
assetsService
|
||||
.load([
|
||||
"lib/markdown/markdown.converter.js",
|
||||
@@ -53,6 +67,12 @@ function MarkdownEditorController($scope, $element, assetsService, editorService
|
||||
return true; // tell the editor that we'll take care of getting the image url
|
||||
});
|
||||
|
||||
//subscribe to the link dialog clicks
|
||||
editor2.hooks.set("insertLinkDialog", function (callback) {
|
||||
openLinkPicker(callback);
|
||||
return true; // tell the editor that we'll take care of getting the link url
|
||||
});
|
||||
|
||||
editor2.hooks.set("onPreviewRefresh", function () {
|
||||
// We must manually update the model as there is no way to hook into the markdown editor events without exstensive edits to the library.
|
||||
if ($scope.model.value !== $("textarea", $element).val()) {
|
||||
|
||||
+11
@@ -82,6 +82,7 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en
|
||||
currentTarget: target,
|
||||
dataTypeKey: $scope.model.dataTypeKey,
|
||||
ignoreUserStartNodes : ($scope.model.config && $scope.model.config.ignoreUserStartNodes) ? $scope.model.config.ignoreUserStartNodes : "0",
|
||||
hideAnchor: $scope.model.config && $scope.model.config.hideAnchor ? true : false,
|
||||
submit: function (model) {
|
||||
if (model.target.url || model.target.anchor) {
|
||||
// if an anchor exists, check that it is appropriately prefixed
|
||||
@@ -143,6 +144,16 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en
|
||||
if ($scope.model.validation && $scope.model.validation.mandatory && !$scope.model.config.minNumber) {
|
||||
$scope.model.config.minNumber = 1;
|
||||
}
|
||||
|
||||
_.each($scope.model.value, function (item){
|
||||
// we must reload the "document" link URLs to match the current editor culture
|
||||
if (item.udi.indexOf("/document/") > 0) {
|
||||
item.url = null;
|
||||
entityResource.getUrlByUdi(item.udi).then(function (data) {
|
||||
item.url = data;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
+33
-17
@@ -41,7 +41,7 @@
|
||||
if (vm.maxItems === 0)
|
||||
vm.maxItems = 1000;
|
||||
|
||||
vm.singleMode = vm.minItems === 1 && vm.maxItems === 1;
|
||||
vm.singleMode = vm.minItems === 1 && vm.maxItems === 1 && model.config.contentTypes.length === 1;;
|
||||
vm.showIcons = Object.toBoolean(model.config.showIcons);
|
||||
vm.wideMode = Object.toBoolean(model.config.hideLabel);
|
||||
vm.hasContentTypes = model.config.contentTypes.length > 0;
|
||||
@@ -131,6 +131,7 @@
|
||||
|
||||
setCurrentNode(newNode);
|
||||
setDirty();
|
||||
validate();
|
||||
};
|
||||
|
||||
vm.openNodeTypePicker = function ($event) {
|
||||
@@ -234,12 +235,22 @@
|
||||
}
|
||||
};
|
||||
|
||||
vm.canDeleteNode = function (idx) {
|
||||
return (vm.nodes.length > vm.minItems)
|
||||
? true
|
||||
: model.config.contentTypes.length > 1;
|
||||
}
|
||||
|
||||
function deleteNode(idx) {
|
||||
vm.nodes.splice(idx, 1);
|
||||
setDirty();
|
||||
updateModel();
|
||||
validate();
|
||||
};
|
||||
vm.requestDeleteNode = function (idx) {
|
||||
if (!vm.canDeleteNode(idx)) {
|
||||
return;
|
||||
}
|
||||
if (model.config.confirmDeletes === true) {
|
||||
localizationService.localizeMany(["content_nestedContentDeleteItem", "general_delete", "general_cancel", "contentTypeEditor_yesDelete"]).then(function (data) {
|
||||
const overlay = {
|
||||
@@ -468,8 +479,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-fill with elementTypes, but only if we have one type to choose from, and if this property is empty.
|
||||
if (vm.singleMode === true && vm.nodes.length === 0 && model.config.minItems > 0) {
|
||||
// Enforce min items if we only have one scaffold type
|
||||
if (vm.nodes.length < vm.minItems && vm.scaffolds.length === 1) {
|
||||
for (var i = vm.nodes.length; i < model.config.minItems; i++) {
|
||||
addNode(vm.scaffolds[0].contentTypeAlias);
|
||||
}
|
||||
@@ -480,6 +491,8 @@
|
||||
setCurrentNode(vm.nodes[0]);
|
||||
}
|
||||
|
||||
validate();
|
||||
|
||||
vm.inited = true;
|
||||
|
||||
updatePropertyActionStates();
|
||||
@@ -585,25 +598,28 @@
|
||||
updateModel();
|
||||
});
|
||||
|
||||
var validate = function () {
|
||||
if (vm.nodes.length < vm.minItems) {
|
||||
$scope.nestedContentForm.minCount.$setValidity("minCount", false);
|
||||
}
|
||||
else {
|
||||
$scope.nestedContentForm.minCount.$setValidity("minCount", true);
|
||||
}
|
||||
|
||||
if (vm.nodes.length > vm.maxItems) {
|
||||
$scope.nestedContentForm.maxCount.$setValidity("maxCount", false);
|
||||
}
|
||||
else {
|
||||
$scope.nestedContentForm.maxCount.$setValidity("maxCount", true);
|
||||
}
|
||||
}
|
||||
|
||||
var watcher = $scope.$watch(
|
||||
function () {
|
||||
return vm.nodes.length;
|
||||
},
|
||||
function () {
|
||||
//Validate!
|
||||
if (vm.nodes.length < vm.minItems) {
|
||||
$scope.nestedContentForm.minCount.$setValidity("minCount", false);
|
||||
}
|
||||
else {
|
||||
$scope.nestedContentForm.minCount.$setValidity("minCount", true);
|
||||
}
|
||||
|
||||
if (vm.nodes.length > vm.maxItems) {
|
||||
$scope.nestedContentForm.maxCount.$setValidity("maxCount", false);
|
||||
}
|
||||
else {
|
||||
$scope.nestedContentForm.maxCount.$setValidity("maxCount", true);
|
||||
}
|
||||
validate();
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
+4
-2
@@ -56,11 +56,13 @@
|
||||
<div class="umb-nested-content__help-text" ng-show="showHelpText">
|
||||
<p>
|
||||
<b><localize key="general_group">Group</localize>:</b><br />
|
||||
Select the group whose properties should be displayed. If left blank, the first group on the element type will be used.
|
||||
<localize key="content_nestedContentGroupHelpText">Select the group whose properties should be displayed. If left blank, the first group on the element type will be used.</localize>
|
||||
</p>
|
||||
<p>
|
||||
<b><localize key="template_template">Template</localize>:</b><br />
|
||||
Enter an angular expression to evaluate against each item for its name. Use <code ng-non-bindable>{{$index}}</code> to display the item index
|
||||
<localize key="content_nestedContentTemplateHelpTextPart1">Enter an angular expression to evaluate against each item for its name. Use</localize>
|
||||
<code ng-non-bindable>{{$index}}</code>
|
||||
<localize key="content_nestedContentTemplateHelpTextPart2">to display the item index</localize>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+5
-5
@@ -2,13 +2,13 @@
|
||||
|
||||
<umb-load-indicator class="mt2" ng-if="!vm.inited"></umb-load-indicator>
|
||||
|
||||
<ng-form name="nestedContentForm">
|
||||
<ng-form name="nestedContentForm" ng-show="vm.inited">
|
||||
|
||||
<div class="umb-nested-content__items" ng-hide="vm.nodes.length === 0" ui-sortable="vm.sortableOptions" ng-model="vm.nodes">
|
||||
|
||||
<div class="umb-nested-content__item" ng-repeat="node in vm.nodes" ng-class="{ 'umb-nested-content__item--active' : vm.currentNode.key === node.key, 'umb-nested-content__item--single' : vm.singleMode }">
|
||||
|
||||
<div class="umb-nested-content__header-bar" ng-click="vm.editNode($index)" ng-hide="vm.singleMode">
|
||||
<div class="umb-nested-content__header-bar" ng-click="vm.editNode($index)" ng-hide="vm.singleMode" umb-auto-focus="{{vm.currentNode.key === node.key ? 'true' : 'false'}}">
|
||||
|
||||
<div class="umb-nested-content__heading"><i ng-if="vm.showIcons" class="icon" ng-class="vm.getIcon($index)"></i><span class="umb-nested-content__item-name" ng-class="{'--has-icon': vm.showIcons}" ng-bind="vm.getName($index)"></span></div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<i class="icon icon-documents" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{vm.labels.copy_icon_title}}</span>
|
||||
</button>
|
||||
<button type="button" class="umb-nested-content__icon umb-nested-content__icon--delete" localize="title" title="general_delete" ng-click="vm.requestDeleteNode($index); $event.stopPropagation();">
|
||||
<button type="button" class="umb-nested-content__icon umb-nested-content__icon--delete" localize="title" title="general_delete" ng-class="{ 'umb-nested-content__icon--disabled': !vm.canDeleteNode($index) }" ng-click="vm.requestDeleteNode($index); $event.stopPropagation();">
|
||||
<i class="icon icon-trash" aria-hidden="true"></i>
|
||||
<span class="sr-only">
|
||||
<localize key="general_delete">Delete</localize>
|
||||
@@ -40,8 +40,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="umb-nested-content__footer-bar" ng-hide="!vm.inited || vm.hasContentTypes === false">
|
||||
<button type="button" class="btn-reset umb-nested-content__add-content umb-focus" ng-class="{ '--disabled': (!vm.scaffolds.length || vm.nodes.length >= maxItems) }" ng-click="vm.openNodeTypePicker($event)" prevent-default>
|
||||
<div class="umb-nested-content__footer-bar" ng-hide="!vm.inited || vm.hasContentTypes === false || vm.singleMode === true">
|
||||
<button type="button" class="btn-reset umb-nested-content__add-content umb-focus" ng-class="{ '--disabled': (!vm.scaffolds.length || vm.nodes.length >= vm.maxItems) }" ng-click="vm.openNodeTypePicker($event)" prevent-default>
|
||||
<localize key="grid_addElement"></localize>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -451,7 +451,7 @@
|
||||
|
||||
var search = _.debounce(function () {
|
||||
$scope.$apply(function () {
|
||||
getUsers();
|
||||
changePageNumber(1);
|
||||
});
|
||||
}, 500);
|
||||
|
||||
@@ -512,7 +512,7 @@
|
||||
}
|
||||
|
||||
updateLocation("userStates", vm.usersOptions.userStates.join(","));
|
||||
getUsers();
|
||||
changePageNumber(1);
|
||||
}
|
||||
|
||||
function setUserGroupFilter(userGroup) {
|
||||
@@ -529,7 +529,7 @@
|
||||
}
|
||||
|
||||
updateLocation("userGroups", vm.usersOptions.userGroups.join(","));
|
||||
getUsers();
|
||||
changePageNumber(1);
|
||||
}
|
||||
|
||||
function setOrderByFilter(value, direction) {
|
||||
|
||||
@@ -7,6 +7,21 @@ body {
|
||||
|
||||
}
|
||||
|
||||
.container, .navbar-static-top .container, .navbar-fixed-top .container, .navbar-fixed-bottom .container {
|
||||
max-width: 1500px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.span3 {
|
||||
width: 220px;
|
||||
width: calc(90% / 12 * 3);
|
||||
}
|
||||
|
||||
.span9 {
|
||||
width: 700px;
|
||||
width: calc(90% / 12 * 9);
|
||||
}
|
||||
|
||||
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
|
||||
font-family: inherit;
|
||||
font-weight: 400;
|
||||
|
||||
@@ -112,6 +112,12 @@
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="4.6.0" />
|
||||
<PackageReference Include="Umbraco.SqlServerCE" Version="4.0.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<ProjectReference Include="..\Umbraco.TestData\Umbraco.TestData.csproj">
|
||||
<Project>{fb5676ed-7a69-492c-b802-e7b24144c0fc}</Project>
|
||||
<Name>Umbraco.TestData</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Abstractions\Umbraco.Abstractions.csproj">
|
||||
<Project>{29aa69d9-b597-4395-8d42-43b1263c240a}</Project>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
{
|
||||
if (success)
|
||||
{
|
||||
@* This message will show if RedirectOnSucces is set to false (default) *@
|
||||
@* This message will show if profileModel.RedirectUrl is not defined (default) *@
|
||||
<p>Profile updated</p>
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
@if (success)
|
||||
{
|
||||
@* This message will show if RedirectOnSucces is set to false (default) *@
|
||||
@* This message will show if registerModel.RedirectUrl is not defined (default) *@
|
||||
<p>Registration succeeded.</p>
|
||||
}
|
||||
else
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
<key alias="SetPermissionsForThePage">Sæt rettigheder for siden %0%</key>
|
||||
<key alias="chooseWhereToCopy">Vælg hvor du vil kopiere</key>
|
||||
<key alias="chooseWhereToMove">Vælg hvortil du vil flytte</key>
|
||||
<key alias="infiniteEditorChooseWhereToMove">Vælg hvor du vil flytte de valgte elementer hen</key>
|
||||
<key alias="toInTheTreeStructureBelow">til i træstrukturen nedenfor</key>
|
||||
<key alias="infiniteEditorChooseWhereToCopy">Vælg hvor du vil kopiere de valgte elementer til</key>
|
||||
<key alias="wasMovedTo">blev flyttet til</key>
|
||||
<key alias="wasCopiedTo">blev kopieret til</key>
|
||||
<key alias="wasDeleted">blev slettet</key>
|
||||
@@ -446,6 +448,7 @@
|
||||
<key alias="viewCacheItem">Se cache element</key>
|
||||
<key alias="relateToOriginalLabel">Relatér til original</key>
|
||||
<key alias="includeDescendants">Inkludér undersider</key>
|
||||
<key alias="theFriendliestCommunity">Det venligste community</key>
|
||||
<key alias="linkToPage">Link til side</key>
|
||||
<key alias="openInNewWindow">Åben linket i et nyt vindue eller fane</key>
|
||||
<key alias="linkToMedia">Link til medie</key>
|
||||
@@ -539,9 +542,6 @@
|
||||
<key alias="anchor">#value eller ?key=value</key>
|
||||
<key alias="enterAlias">Indtast alias...</key>
|
||||
<key alias="generatingAlias">Genererer alias...</key>
|
||||
<key alias="a11yCreateItem">Opret element</key>
|
||||
<key alias="a11yEdit">Rediger</key>
|
||||
<key alias="a11yName">Navn</key>
|
||||
</area>
|
||||
<area alias="editcontenttype">
|
||||
<key alias="createListView" version="7.2">Opret brugerdefineret listevisning</key>
|
||||
@@ -751,6 +751,9 @@
|
||||
<key alias="current">nuværende</key>
|
||||
<key alias="embed">Indlejring</key>
|
||||
<key alias="selected">valgt</key>
|
||||
<key alias="other">Andet</key>
|
||||
<key alias="articles">Artikler</key>
|
||||
<key alias="videos">Videoer</key>
|
||||
</area>
|
||||
<area alias="colors">
|
||||
<key alias="blue">Blå</key>
|
||||
@@ -1115,7 +1118,10 @@ Mange hilsner fra Umbraco robotten
|
||||
<key alias="forms">Formularer</key>
|
||||
</area>
|
||||
<area alias="help">
|
||||
<key alias="tours">Tours</key>
|
||||
<key alias="theBestUmbracoVideoTutorials">De bedste Umbraco video tutorials</key>
|
||||
<key alias="umbracoForum">Besøg our.umbraco.com</key>
|
||||
<key alias="umbracoTv">Besøg umbraco.tv</key>
|
||||
</area>
|
||||
<area alias="settings">
|
||||
<key alias="defaulttemplate">Standardskabelon</key>
|
||||
@@ -1733,6 +1739,9 @@ Mange hilsner fra Umbraco robotten
|
||||
<key alias="currentLanguage">Aktivt sprog</key>
|
||||
<key alias="switchLanguage">Skift sprog til</key>
|
||||
<key alias="createNewFolder">Opret ny mappe</key>
|
||||
<key alias="createItem">Opret element</key>
|
||||
<key alias="edit">Rediger</key>
|
||||
<key alias="name">Navn</key>
|
||||
</area>
|
||||
<area alias="references">
|
||||
<key alias="tabName">Referencer</key>
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
<key alias="chooseWhereToCopy">Choose where to copy</key>
|
||||
<key alias="chooseWhereToMove">Choose where to move</key>
|
||||
<key alias="toInTheTreeStructureBelow">to in the tree structure below</key>
|
||||
<key alias="infiniteEditorChooseWhereToCopy">Choose where to copy the selected item(s)</key>
|
||||
<key alias="infiniteEditorChooseWhereToMove">Choose where to move the selected item(s)</key>
|
||||
<key alias="wasMovedTo">was moved to</key>
|
||||
<key alias="wasCopiedTo">was copied to</key>
|
||||
<key alias="wasDeleted">was deleted</key>
|
||||
@@ -280,6 +282,9 @@
|
||||
<key alias="nestedContentNoContentTypes">No content types are configured for this property.</key>
|
||||
<key alias="nestedContentAddElementType">Add element type</key>
|
||||
<key alias="nestedContentSelectElementTypeModalTitle">Select element type</key>
|
||||
<key alias="nestedContentGroupHelpText">Select the group whose properties should be displayed. If left blank, the first group on the element type will be used.</key>
|
||||
<key alias="nestedContentTemplateHelpTextPart1">Enter an angular expression to evaluate against each item for its name. Use</key>
|
||||
<key alias="nestedContentTemplateHelpTextPart2">to display the item index</key>
|
||||
<key alias="addTextBox">Add another text box</key>
|
||||
<key alias="removeTextBox">Remove this text box</key>
|
||||
<key alias="contentRoot">Content root</key>
|
||||
@@ -759,6 +764,11 @@
|
||||
<key alias="current">current</key>
|
||||
<key alias="embed">Embed</key>
|
||||
<key alias="selected">selected</key>
|
||||
<key alias="other">Other</key>
|
||||
<key alias="articles">Articles</key>
|
||||
<key alias="videos">Videos</key>
|
||||
<key alias="clear">Clear</key>
|
||||
<key alias="installing">Installing</key>
|
||||
</area>
|
||||
<area alias="colors">
|
||||
<key alias="blue">Blue</key>
|
||||
@@ -1339,7 +1349,10 @@ To manage your website, simply open the Umbraco back office and start adding con
|
||||
<key alias="users">Users</key>
|
||||
</area>
|
||||
<area alias="help">
|
||||
<key alias="tours">Tours</key>
|
||||
<key alias="theBestUmbracoVideoTutorials">The best Umbraco video tutorials</key>
|
||||
<key alias="umbracoForum">Visit our.umbraco.com</key>
|
||||
<key alias="umbracoTv">Visit umbraco.tv</key>
|
||||
</area>
|
||||
<area alias="settings">
|
||||
<key alias="defaulttemplate">Default template</key>
|
||||
@@ -1637,6 +1650,7 @@ To manage your website, simply open the Umbraco back office and start adding con
|
||||
<key alias="elementType">Element type</key>
|
||||
<key alias="elementHeading">Is an Element type</key>
|
||||
<key alias="elementDescription">An Element type is meant to be used for instance in Nested Content, and not in the tree.</key>
|
||||
<key alias="elementCannotToggle">A document type cannot be changed to an Element type once it has been used to create one or more content items.</key>
|
||||
<key alias="elementDoesNotSupport">This is not applicable for an Element type</key>
|
||||
<key alias="propertyHasChanges">You have made changes to this property. Are you sure you want to discard them?</key>
|
||||
</area>
|
||||
@@ -2202,6 +2216,10 @@ To manage your website, simply open the Umbraco back office and start adding con
|
||||
<key alias="redirectDashboardSearchLabel">Search the redirect dashboard</key>
|
||||
<key alias="userGroupSearchLabel">Search the user group section</key>
|
||||
<key alias="userSearchLabel">Search the users section</key>
|
||||
<key alias="createItem">Create item</key>
|
||||
<key alias="create">Create</key>
|
||||
<key alias="edit">Edit</key>
|
||||
<key alias="name">Name</key>
|
||||
</area>
|
||||
<area alias="references">
|
||||
<key alias="tabName">References</key>
|
||||
@@ -2367,5 +2385,9 @@ To manage your website, simply open the Umbraco back office and start adding con
|
||||
<area alias="startupDashboard">
|
||||
<key alias="fallbackHeadline">Welcome to The Friendly CMS</key>
|
||||
<key alias="fallbackDescription">Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible.</key>
|
||||
</area>
|
||||
<area alias="formsDashboard">
|
||||
<key alias="formsHeadline">Umbraco Forms</key>
|
||||
<key alias="formsDescription">Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it!</key>
|
||||
</area>
|
||||
</language>
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
<key alias="chooseWhereToCopy">Choose where to copy</key>
|
||||
<key alias="chooseWhereToMove">Choose where to move</key>
|
||||
<key alias="toInTheTreeStructureBelow">to in the tree structure below</key>
|
||||
<key alias="infiniteEditorChooseWhereToCopy">Choose where to copy the selected item(s)</key>
|
||||
<key alias="infiniteEditorChooseWhereToMove">Choose where to move the selected item(s)</key>
|
||||
<key alias="wasMovedTo">was moved to</key>
|
||||
<key alias="wasCopiedTo">was copied to</key>
|
||||
<key alias="wasDeleted">was deleted</key>
|
||||
@@ -284,6 +286,9 @@
|
||||
<key alias="nestedContentNoContentTypes">No content types are configured for this property.</key>
|
||||
<key alias="nestedContentAddElementType">Add element type</key>
|
||||
<key alias="nestedContentSelectElementTypeModalTitle">Select element type</key>
|
||||
<key alias="nestedContentGroupHelpText">Select the group whose properties should be displayed. If left blank, the first group on the element type will be used.</key>
|
||||
<key alias="nestedContentTemplateHelpTextPart1">Enter an angular expression to evaluate against each item for its name. Use</key>
|
||||
<key alias="nestedContentTemplateHelpTextPart2">to display the item index</key>
|
||||
<key alias="addTextBox">Add another text box</key>
|
||||
<key alias="removeTextBox">Remove this text box</key>
|
||||
<key alias="contentRoot">Content root</key>
|
||||
@@ -551,10 +556,6 @@
|
||||
<key alias="anchor">#value or ?key=value</key>
|
||||
<key alias="enterAlias">Enter alias...</key>
|
||||
<key alias="generatingAlias">Generating alias...</key>
|
||||
<key alias="a11yCreateItem">Create item</key>
|
||||
<key alias="a11yCreate">Create</key>
|
||||
<key alias="a11yEdit">Edit</key>
|
||||
<key alias="a11yName">Name</key>
|
||||
</area>
|
||||
<area alias="editcontenttype">
|
||||
<key alias="createListView" version="7.2">Create custom list view</key>
|
||||
@@ -764,6 +765,11 @@
|
||||
<key alias="current">current</key>
|
||||
<key alias="embed">Embed</key>
|
||||
<key alias="selected">selected</key>
|
||||
<key alias="other">Other</key>
|
||||
<key alias="articles">Articles</key>
|
||||
<key alias="videos">Videos</key>
|
||||
<key alias="clear">Clear</key>
|
||||
<key alias="installing">Installing</key>
|
||||
</area>
|
||||
<area alias="colors">
|
||||
<key alias="blue">Blue</key>
|
||||
@@ -1339,7 +1345,10 @@ To manage your website, simply open the Umbraco back office and start adding con
|
||||
<key alias="users">Users</key>
|
||||
</area>
|
||||
<area alias="help">
|
||||
<key alias="tours">Tours</key>
|
||||
<key alias="theBestUmbracoVideoTutorials">The best Umbraco video tutorials</key>
|
||||
<key alias="umbracoForum">Visit our.umbraco.com</key>
|
||||
<key alias="umbracoTv">Visit umbraco.tv</key>
|
||||
</area>
|
||||
<area alias="settings">
|
||||
<key alias="defaulttemplate">Default template</key>
|
||||
@@ -1649,9 +1658,10 @@ To manage your website, simply open the Umbraco back office and start adding con
|
||||
<key alias="variantsDescription">Allow editors to create content of this type in different languages.</key>
|
||||
<key alias="allowVaryByCulture">Allow varying by culture</key>
|
||||
<key alias="elementType">Element type</key>
|
||||
<key alias="elementHeading">Is an Element type</key>
|
||||
<key alias="elementDescription">An Element type is meant to be used for instance in Nested Content, and not in the tree.</key>
|
||||
<key alias="elementDoesNotSupport">This is not applicable for an Element type</key>
|
||||
<key alias="elementHeading">Is an element type</key>
|
||||
<key alias="elementDescription">An element type is meant to be used for instance in Nested Content, and not in the tree.</key>
|
||||
<key alias="elementCannotToggle">A document type cannot be changed to an element type once it has been used to create one or more content items.</key>
|
||||
<key alias="elementDoesNotSupport">This is not applicable for an element type</key>
|
||||
<key alias="propertyHasChanges">You have made changes to this property. Are you sure you want to discard them?</key>
|
||||
</area>
|
||||
<area alias="languages">
|
||||
@@ -2218,6 +2228,10 @@ To manage your website, simply open the Umbraco back office and start adding con
|
||||
<key alias="redirectDashboardSearchLabel">Search the redirect dashboard</key>
|
||||
<key alias="userGroupSearchLabel">Search the user group section</key>
|
||||
<key alias="userSearchLabel">Search the users section</key>
|
||||
<key alias="createItem">Create item</key>
|
||||
<key alias="create">Create</key>
|
||||
<key alias="edit">Edit</key>
|
||||
<key alias="name">Name</key>
|
||||
</area>
|
||||
<area alias="references">
|
||||
<key alias="tabName">References</key>
|
||||
@@ -2384,4 +2398,8 @@ To manage your website, simply open the Umbraco back office and start adding con
|
||||
<key alias="fallbackHeadline">Welcome to The Friendly CMS</key>
|
||||
<key alias="fallbackDescription">Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible.</key>
|
||||
</area>
|
||||
<area alias="formsDashboard">
|
||||
<key alias="formsHeadline">Umbraco Forms</key>
|
||||
<key alias="formsDescription">Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it!</key>
|
||||
</area>
|
||||
</language>
|
||||
|
||||
@@ -73,6 +73,13 @@ namespace Umbraco.Web.Editors
|
||||
return Services.ContentTypeService.Count();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[UmbracoTreeAuthorize(Constants.Trees.DocumentTypes)]
|
||||
public bool HasContentNodes(int id)
|
||||
{
|
||||
return Services.ContentTypeService.HasContentNodes(id);
|
||||
}
|
||||
|
||||
public DocumentTypeDisplay GetById(int id)
|
||||
{
|
||||
var ct = Services.ContentTypeService.Get(id);
|
||||
@@ -428,11 +435,11 @@ namespace Umbraco.Web.Editors
|
||||
}
|
||||
|
||||
var contentType = Services.ContentTypeBaseServices.GetContentTypeOf(contentItem);
|
||||
var ids = contentType.AllowedContentTypes.Select(x => x.Id.Value).ToArray();
|
||||
var ids = contentType.AllowedContentTypes.OrderBy(c => c.SortOrder).Select(x => x.Id.Value).ToArray();
|
||||
|
||||
if (ids.Any() == false) return Enumerable.Empty<ContentTypeBasic>();
|
||||
|
||||
types = Services.ContentTypeService.GetAll(ids).ToList();
|
||||
types = Services.ContentTypeService.GetAll(ids).OrderBy(c => ids.IndexOf(c.Id)).ToList();
|
||||
}
|
||||
|
||||
var basics = types.Where(type => type.IsElement == false).Select(Mapper.Map<IContentType, ContentTypeBasic>).ToList();
|
||||
@@ -455,7 +462,7 @@ namespace Umbraco.Web.Editors
|
||||
}
|
||||
}
|
||||
|
||||
return basics;
|
||||
return basics.OrderBy(c => contentId == Constants.System.Root ? c.Name : string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -219,6 +219,35 @@ namespace Umbraco.Web.Editors
|
||||
throw new HttpResponseException(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the url of an entity
|
||||
/// </summary>
|
||||
/// <param name="udi">UDI of the entity to fetch URL for</param>
|
||||
/// <param name="culture">The culture to fetch the URL for</param>
|
||||
/// <returns>The URL or path to the item</returns>
|
||||
public HttpResponseMessage GetUrl(Udi udi, string culture = "*")
|
||||
{
|
||||
var intId = Services.EntityService.GetId(udi);
|
||||
if (!intId.Success)
|
||||
throw new HttpResponseException(HttpStatusCode.NotFound);
|
||||
UmbracoEntityTypes entityType;
|
||||
switch(udi.EntityType)
|
||||
{
|
||||
case Constants.UdiEntityType.Document:
|
||||
entityType = UmbracoEntityTypes.Document;
|
||||
break;
|
||||
case Constants.UdiEntityType.Media:
|
||||
entityType = UmbracoEntityTypes.Media;
|
||||
break;
|
||||
case Constants.UdiEntityType.Member:
|
||||
entityType = UmbracoEntityTypes.Member;
|
||||
break;
|
||||
default:
|
||||
throw new HttpResponseException(HttpStatusCode.NotFound);
|
||||
}
|
||||
return GetUrl(intId.Result, entityType, culture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the url of an entity
|
||||
/// </summary>
|
||||
@@ -316,7 +345,9 @@ namespace Umbraco.Web.Editors
|
||||
[HttpGet]
|
||||
public UrlAndAnchors GetUrlAndAnchors(int id, string culture = "*")
|
||||
{
|
||||
var url = UmbracoContext.UrlProvider.GetUrl(id);
|
||||
culture = culture ?? ClientCulture();
|
||||
|
||||
var url = UmbracoContext.UrlProvider.GetUrl(id, culture: culture);
|
||||
var anchorValues = Services.ContentService.GetAnchorValuesFromRTEs(id, culture);
|
||||
return new UrlAndAnchors(url, anchorValues);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user