Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a73bbb900 | ||
|
|
99629e4712 | ||
|
|
27ae938099 | ||
|
|
edb69eea36 | ||
|
|
30bb416ff8 | ||
|
|
a9b8090d6b | ||
|
|
76ecb295cc | ||
|
|
ce817551a5 | ||
|
|
32cd9fed49 | ||
|
|
cabb91dbe1 | ||
|
|
633bc8fd8a | ||
|
|
9de4cf4444 | ||
|
|
ffe9a14664 | ||
|
|
1fb4b905c2 | ||
|
|
4b8a16af7c | ||
|
|
f6c4af745f | ||
|
|
6dd8cb496b | ||
|
|
8d6394e8e3 | ||
|
|
159f92d6f8 | ||
|
|
75f8c09e85 | ||
|
|
62de668380 | ||
|
|
96349e4754 | ||
|
|
1ec98139c6 | ||
|
|
7bb85c9ed6 | ||
|
|
25ac6b275e | ||
|
|
db8fe55d08 | ||
|
|
86fcb8e715 | ||
|
|
c85486b866 | ||
|
|
4e2302ab6e | ||
|
|
a87e7a12d0 | ||
|
|
2fc8f62195 | ||
|
|
586c4e8db3 | ||
|
|
5c663fd0f0 | ||
|
|
4f89cb1a34 | ||
|
|
424db16ab1 | ||
|
|
e9be3dd4a5 | ||
|
|
8ff1d49346 | ||
|
|
1473e41a20 | ||
|
|
2038898f4c | ||
|
|
40c08f81c4 |
@@ -15,8 +15,8 @@
|
||||
<div class="level{{item.level}}"><span class="xref">{{item.name.0.value}}</span></div>
|
||||
</div>
|
||||
{{/inheritance.0}}
|
||||
<h6><strong>{{__global.namespace}}</strong>:{{namespace}}</h6>
|
||||
<h6><strong>{{__global.assembly}}</strong>:{{assemblies.0}}.dll</h6>
|
||||
<h6><strong>{{__global.namespace}}</strong>: {{{namespace.specName.0.value}}}</h6>
|
||||
<h6><strong>{{__global.assembly}}</strong>: {{assemblies.0}}.dll</h6>
|
||||
<h5 id="{{id}}_syntax">{{__global.syntax}}</h5>
|
||||
<div class="codewrapper">
|
||||
<pre><code class="lang-{{_lang}} hljs">{{syntax.content.0.value}}</code></pre>
|
||||
|
||||
+2
-2
@@ -18,5 +18,5 @@ using System.Resources;
|
||||
[assembly: AssemblyVersion("8.0.0")]
|
||||
|
||||
// these are FYI and changed automatically
|
||||
[assembly: AssemblyFileVersion("8.18.0")]
|
||||
[assembly: AssemblyInformationalVersion("8.18.0-rc")]
|
||||
[assembly: AssemblyFileVersion("8.18.3")]
|
||||
[assembly: AssemblyInformationalVersion("8.18.3")]
|
||||
|
||||
@@ -213,6 +213,7 @@ namespace Umbraco.Core.Migrations.Upgrade
|
||||
|
||||
//FINAL
|
||||
To<AddContentVersionCleanupFeature>("{8BAF5E6C-DCB7-41AE-824F-4215AE4F1F98}");
|
||||
To<NoopMigration>("{03482BB0-CF13-475C-845E-ECB8319DBE3C}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,37 @@
|
||||
using System.Runtime.Serialization;
|
||||
using Umbraco.Core.Models.Entities;
|
||||
|
||||
namespace Umbraco.Core.Models.ContentEditing
|
||||
{
|
||||
[DataContract(Name = "historyCleanup", Namespace = "")]
|
||||
public class HistoryCleanup
|
||||
public class HistoryCleanup : BeingDirtyBase
|
||||
{
|
||||
private bool _preventCleanup;
|
||||
|
||||
[DataMember(Name = "preventCleanup")]
|
||||
public bool PreventCleanup { get; set; }
|
||||
public bool PreventCleanup
|
||||
{
|
||||
get => _preventCleanup;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _preventCleanup, nameof(PreventCleanup));
|
||||
}
|
||||
|
||||
private int? _keepAllVersionsNewerThanDays;
|
||||
|
||||
[DataMember(Name = "keepAllVersionsNewerThanDays")]
|
||||
public int? KeepAllVersionsNewerThanDays { get; set; }
|
||||
public int? KeepAllVersionsNewerThanDays
|
||||
{
|
||||
get => _keepAllVersionsNewerThanDays;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _keepAllVersionsNewerThanDays, nameof(KeepAllVersionsNewerThanDays));
|
||||
}
|
||||
|
||||
private int? _keepLatestVersionPerDayForDays;
|
||||
|
||||
[DataMember(Name = "keepLatestVersionPerDayForDays")]
|
||||
public int? KeepLatestVersionPerDayForDays { get; set; }
|
||||
public int? KeepLatestVersionPerDayForDays
|
||||
{
|
||||
get => _keepLatestVersionPerDayForDays;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _keepLatestVersionPerDayForDays,
|
||||
nameof(KeepLatestVersionPerDayForDays));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using Umbraco.Core.Models.ContentEditing;
|
||||
|
||||
@@ -50,8 +50,6 @@ namespace Umbraco.Core.Models
|
||||
/// <inheritdoc />
|
||||
public override bool SupportsPublishing => SupportsPublishingConst;
|
||||
|
||||
|
||||
|
||||
//Custom comparer for enumerable
|
||||
private static readonly DelegateEqualityComparer<IEnumerable<ITemplate>> TemplateComparer = new DelegateEqualityComparer<IEnumerable<ITemplate>>(
|
||||
(templates, enumerable) => templates.UnsortedSequenceEqual(enumerable),
|
||||
@@ -98,7 +96,14 @@ namespace Umbraco.Core.Models
|
||||
}
|
||||
}
|
||||
|
||||
public HistoryCleanup HistoryCleanup { get; set; }
|
||||
private HistoryCleanup _historyCleanup;
|
||||
|
||||
public HistoryCleanup HistoryCleanup
|
||||
{
|
||||
get => _historyCleanup;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _historyCleanup, nameof(HistoryCleanup));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Determines if AllowedTemplates contains templateId
|
||||
@@ -165,5 +170,15 @@ namespace Umbraco.Core.Models
|
||||
|
||||
/// <inheritdoc />
|
||||
IContentType IContentType.DeepCloneWithResetIdentities(string newAlias) => (IContentType)DeepCloneWithResetIdentities(newAlias);
|
||||
|
||||
|
||||
public override bool IsDirty()
|
||||
{
|
||||
bool dirtyEntity = base.IsDirty();
|
||||
|
||||
bool dirtyHistoryCleanup = HistoryCleanup.IsDirty();
|
||||
|
||||
return dirtyEntity || dirtyHistoryCleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Umbraco.Core.Models.Entities
|
||||
public abstract class BeingDirtyBase : IRememberBeingDirty
|
||||
{
|
||||
private bool _withChanges = true; // should we track changes?
|
||||
private bool _withChangedEvent = true; // should we fire the event when changes are registered
|
||||
private Dictionary<string, bool> _currentChanges; // which properties have changed?
|
||||
private Dictionary<string, bool> _savedChanges; // which properties had changed at last commit?
|
||||
|
||||
@@ -117,7 +118,10 @@ namespace Umbraco.Core.Models.Entities
|
||||
|
||||
_currentChanges[propertyName] = true;
|
||||
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
if (_withChangedEvent)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -136,6 +140,22 @@ namespace Umbraco.Core.Models.Entities
|
||||
_withChanges = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disables the property changed event.
|
||||
/// </summary>
|
||||
protected void DisablePropertyChangedEvent()
|
||||
{
|
||||
_withChangedEvent = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables the property changed event.
|
||||
/// </summary>
|
||||
protected void EnablePropertyChangedEvent()
|
||||
{
|
||||
_withChangedEvent = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a property value, detects changes and manages the dirty flag.
|
||||
/// </summary>
|
||||
|
||||
@@ -103,8 +103,8 @@ namespace Umbraco.Core.Persistence
|
||||
|
||||
if (string.IsNullOrWhiteSpace(programFiles)) return;
|
||||
|
||||
// detect 14, 13, 12, 11
|
||||
for (var i = 14; i > 10; i--)
|
||||
// detect (17, 16) 15, 14, 13, 12, 11 - future-proofing by a couple of versions
|
||||
for (var i = 17; i > 10; i--)
|
||||
{
|
||||
var exe = Path.Combine(programFiles, $@"Microsoft SQL Server\{i}0\Tools\Binn\SqlLocalDB.exe");
|
||||
if (File.Exists(exe) == false) continue;
|
||||
|
||||
@@ -174,13 +174,13 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID");
|
||||
var urlHash = url.GenerateHash<SHA1>();
|
||||
var sql = GetBaseQuery(false)
|
||||
.Where<RedirectUrlDto>(x => x.Url == url && x.UrlHash == urlHash &&
|
||||
(x.Culture == culture.ToLower() || x.Culture == string.Empty))
|
||||
(x.Culture == culture.ToLower() || x.Culture == null || x.Culture == string.Empty))
|
||||
.OrderByDescending<RedirectUrlDto>(x => x.CreateDateUtc);
|
||||
var dtos = Database.Fetch<RedirectUrlDto>(sql);
|
||||
var dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower());
|
||||
|
||||
if (dto == null)
|
||||
dto = dtos.FirstOrDefault(f => f.Culture == string.Empty);
|
||||
dto = dtos.FirstOrDefault(f => string.IsNullOrWhiteSpace(f.Culture));
|
||||
|
||||
return dto == null ? null : Map(dto);
|
||||
}
|
||||
|
||||
@@ -145,16 +145,25 @@ namespace Umbraco.Core.PropertyEditors
|
||||
/// <returns></returns>
|
||||
internal Attempt<object> TryConvertValueToCrlType(object value)
|
||||
{
|
||||
if (value is JToken jsonValue)
|
||||
if (value is JValue jsonValue)
|
||||
{
|
||||
if (jsonValue is JContainer && jsonValue.HasValues == false)
|
||||
// Calling the "ToString(Formatting)" method on a string value results in a new
|
||||
// string enclosed in double quotes (which we don't want), as the method is declared
|
||||
// in the JToken class. Calling either of the "ToString()" or "ToString(CultureInfo)"
|
||||
// methods hits the overridden methods in the JValue class, which correctly doesn't
|
||||
// enclose the value in double quotes.
|
||||
value = jsonValue.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
else if (value is JToken jsonToken)
|
||||
{
|
||||
if (jsonToken is JContainer && jsonToken.HasValues == false)
|
||||
{
|
||||
// Empty JSON array/object
|
||||
value = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = jsonValue.ToString(Formatting.None);
|
||||
value = jsonToken.ToString(Formatting.None);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Core.Telemetry
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to get and create the site identifier
|
||||
/// </summary>
|
||||
public interface ISiteIdentifierService
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the site identifier
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool TryGetSiteIdentifier(out Guid siteIdentifier);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the site identifier or otherwise create it if it doesn't exist.
|
||||
/// </summary>
|
||||
/// <param name="siteIdentifier"></param>
|
||||
/// <returns></returns>
|
||||
bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the site identifier and writes it to config.
|
||||
/// </summary>
|
||||
bool TryCreateSiteIdentifier(out Guid createdGuid);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.Manifest;
|
||||
using Umbraco.Core.Telemetry.Models;
|
||||
|
||||
@@ -10,7 +9,7 @@ namespace Umbraco.Core.Telemetry
|
||||
/// <inheritdoc/>
|
||||
internal class TelemetryService : ITelemetryService
|
||||
{
|
||||
private readonly IUmbracoSettingsSection _settings;
|
||||
private readonly ISiteIdentifierService _siteIdentifierService;
|
||||
private readonly ManifestParser _manifestParser;
|
||||
|
||||
/// <summary>
|
||||
@@ -18,16 +17,16 @@ namespace Umbraco.Core.Telemetry
|
||||
/// </summary>
|
||||
public TelemetryService(
|
||||
ManifestParser manifestParser,
|
||||
IUmbracoSettingsSection settings)
|
||||
ISiteIdentifierService siteIdentifierService)
|
||||
{
|
||||
_manifestParser = manifestParser;
|
||||
_settings = settings;
|
||||
_siteIdentifierService = siteIdentifierService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGetTelemetryReportData(out TelemetryReportData telemetryReportData)
|
||||
{
|
||||
if (TryGetTelemetryId(out Guid telemetryId) is false)
|
||||
if (_siteIdentifierService.TryGetOrCreateSiteIdentifier(out Guid telemetryId) is false)
|
||||
{
|
||||
telemetryReportData = null;
|
||||
return false;
|
||||
@@ -42,20 +41,6 @@ namespace Umbraco.Core.Telemetry
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryGetTelemetryId(out Guid telemetryId)
|
||||
{
|
||||
// Parse telemetry string as a GUID & verify its a GUID and not some random string
|
||||
// since users may have messed with or decided to empty the app setting or put in something random
|
||||
if (Guid.TryParse(_settings.BackOffice.Id, out var parsedTelemetryId) is false)
|
||||
{
|
||||
telemetryId = Guid.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
telemetryId = parsedTelemetryId;
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEnumerable<PackageTelemetry> GetPackageTelemetry()
|
||||
{
|
||||
List<PackageTelemetry> packages = new ();
|
||||
|
||||
@@ -400,6 +400,7 @@
|
||||
<Compile Include="Services\DateTypeServiceExtensions.cs" />
|
||||
<Compile Include="Services\PropertyValidationService.cs" />
|
||||
<Compile Include="Composing\TypeCollectionBuilderBase.cs" />
|
||||
<Compile Include="Telemetry\ISiteIdentifierService.cs" />
|
||||
<Compile Include="Telemetry\ITelemetryService.cs" />
|
||||
<Compile Include="Telemetry\Models\PackageTelemetry.cs" />
|
||||
<Compile Include="Telemetry\Models\TelemetryReportData.cs" />
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.ContentEditing;
|
||||
using Umbraco.Tests.TestHelpers.Entities;
|
||||
using Umbraco.Tests.Testing;
|
||||
|
||||
namespace Umbraco.Tests.Models
|
||||
{
|
||||
[TestFixture]
|
||||
public class ContentTypeHistoryCleanupTests : UmbracoTestBase
|
||||
{
|
||||
[Test]
|
||||
public void Changing_Keep_all_Makes_ContentType_Dirty()
|
||||
{
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
|
||||
Assert.IsFalse(contentType.IsDirty());
|
||||
|
||||
var newValue = 2;
|
||||
contentType.HistoryCleanup.KeepAllVersionsNewerThanDays = newValue;
|
||||
Assert.IsTrue(contentType.IsDirty());
|
||||
Assert.AreEqual(newValue, contentType.HistoryCleanup.KeepAllVersionsNewerThanDays);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Changing_Keep_latest_Makes_ContentType_Dirty()
|
||||
{
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
|
||||
Assert.IsFalse(contentType.IsDirty());
|
||||
|
||||
var newValue = 2;
|
||||
contentType.HistoryCleanup.KeepLatestVersionPerDayForDays = newValue;
|
||||
Assert.IsTrue(contentType.IsDirty());
|
||||
Assert.AreEqual(newValue, contentType.HistoryCleanup.KeepLatestVersionPerDayForDays);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Changing_Prevent_Cleanup_Makes_ContentType_Dirty()
|
||||
{
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
|
||||
Assert.IsFalse(contentType.IsDirty());
|
||||
|
||||
var newValue = true;
|
||||
contentType.HistoryCleanup.PreventCleanup = newValue;
|
||||
Assert.IsTrue(contentType.IsDirty());
|
||||
Assert.AreEqual(newValue, contentType.HistoryCleanup.PreventCleanup);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Replacing_History_Cleanup_Registers_As_Dirty()
|
||||
{
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
Assert.IsFalse(contentType.IsDirty());
|
||||
|
||||
contentType.HistoryCleanup = new HistoryCleanup();
|
||||
|
||||
Assert.IsTrue(contentType.IsDirty());
|
||||
Assert.IsTrue(contentType.IsPropertyDirty(nameof(contentType.HistoryCleanup)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Replacing_History_Cleanup_Removes_Old_Dirty_History_Properties()
|
||||
{
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
|
||||
contentType.Alias = "NewValue";
|
||||
contentType.HistoryCleanup.KeepAllVersionsNewerThanDays = 2;
|
||||
|
||||
contentType.PropertyChanged += (sender, args) =>
|
||||
{
|
||||
// Ensure that property changed is only invoked for history cleanup
|
||||
Assert.AreEqual(nameof(contentType.HistoryCleanup), args.PropertyName);
|
||||
};
|
||||
|
||||
// Since we're replacing the entire HistoryCleanup the changed property is no longer dirty, the entire HistoryCleanup is
|
||||
contentType.HistoryCleanup = new HistoryCleanup();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsTrue(contentType.IsDirty());
|
||||
Assert.IsFalse(contentType.WasDirty());
|
||||
Assert.AreEqual(2, contentType.GetDirtyProperties().Count());
|
||||
Assert.IsTrue(contentType.IsPropertyDirty(nameof(contentType.HistoryCleanup)));
|
||||
Assert.IsTrue(contentType.IsPropertyDirty(nameof(contentType.Alias)));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Old_History_Cleanup_Reference_Doesnt_Make_Content_Type_Dirty()
|
||||
{
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
var oldHistoryCleanup = contentType.HistoryCleanup;
|
||||
|
||||
contentType.HistoryCleanup = new HistoryCleanup();
|
||||
contentType.ResetDirtyProperties();
|
||||
contentType.ResetWereDirtyProperties();
|
||||
|
||||
oldHistoryCleanup.KeepAllVersionsNewerThanDays = 2;
|
||||
|
||||
Assert.IsFalse(contentType.IsDirty());
|
||||
Assert.IsFalse(contentType.WasDirty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,7 @@
|
||||
<Compile Include="ModelsBuilder\StringExtensions.cs" />
|
||||
<Compile Include="ModelsBuilder\UmbracoApplicationTests.cs" />
|
||||
<Compile Include="Models\ContentScheduleTests.cs" />
|
||||
<Compile Include="Models\ContentTypeHistoryCleanupTests.cs" />
|
||||
<Compile Include="Models\CultureImpactTests.cs" />
|
||||
<Compile Include="Models\ImageProcessorImageUrlGeneratorTest.cs" />
|
||||
<Compile Include="Models\PathValidationTests.cs" />
|
||||
|
||||
+15
-7
@@ -182,23 +182,31 @@
|
||||
}
|
||||
);
|
||||
|
||||
$scope.propertyEditorDisabled = function (property) {
|
||||
$scope.propertyEditorInherited = function (property) {
|
||||
if (property.unlockInvariantValue) {
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
var contentLanguage = $scope.content.language;
|
||||
|
||||
var canEditCulture = !contentLanguage ||
|
||||
// If the property culture equals the content culture it can be edited
|
||||
property.culture === contentLanguage.culture ||
|
||||
// A culture-invariant property can only be edited by the default language variant
|
||||
(property.culture == null && contentLanguage.isDefault);
|
||||
// If the property culture equals the content culture it can be edited
|
||||
property.culture === contentLanguage.culture ||
|
||||
// A culture-invariant property can only be edited by the default language variant
|
||||
(property.culture == null && contentLanguage.isDefault);
|
||||
|
||||
var canEditSegment = property.segment === $scope.content.segment;
|
||||
|
||||
return !canEditCulture || !canEditSegment;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.propertyEditorDisabled = function (property) {
|
||||
if (property.readonly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $scope.propertyEditorInherited(property);
|
||||
};
|
||||
}
|
||||
|
||||
var directive = {
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
});
|
||||
|
||||
var saveProperties = _.map(realProperties, function (p) {
|
||||
var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile', 'isSensitiveData', 'allowCultureVariant', 'allowSegmentVariant', 'labelOnTop');
|
||||
var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'dataTypeKey', 'groupId', 'memberCanEdit', 'showOnMemberProfile', 'isSensitiveData', 'allowCultureVariant', 'allowSegmentVariant', 'labelOnTop');
|
||||
return saveProperty;
|
||||
});
|
||||
|
||||
|
||||
@@ -128,13 +128,13 @@
|
||||
|
||||
<!-- Links -->
|
||||
<div class="umb-help-section" data-element="help-links" ng-if="vm.hasAccessToSettings">
|
||||
<a data-element="help-link-umbraco-tv" class="umb-help-badge" href="https://umbraco.tv?utm_source=core&utm_medium=help&utm_content=link&utm_campaign=tv" target="_blank" rel="noopener">
|
||||
<a data-element="help-link-umbraco-tv" class="umb-help-badge" href="https://umbra.co/ulb" target="_blank" rel="noopener">
|
||||
<umb-icon icon="icon-tv-old" class="umb-help-badge__icon"></umb-icon>
|
||||
<div class="umb-help-badge__title">
|
||||
<localize key="help_umbracoTv">Visit umbraco.tv</localize>
|
||||
<localize key="help_umbracoLearningBase">Watch our free tutorial videos</localize>
|
||||
</div>
|
||||
<small>
|
||||
<localize key="help_theBestUmbracoVideoTutorials">The best Umbraco video tutorials</localize>
|
||||
<localize key="help_umbracoLearningBaseDescription">on the Umbraco Learning Base</localize>
|
||||
</small>
|
||||
</a>
|
||||
|
||||
|
||||
+53
-46
@@ -3,25 +3,35 @@ angular.module("umbraco")
|
||||
function ($scope, localizationService, entityResource, editorService, overlayService, eventsService, mediaHelper) {
|
||||
|
||||
var unsubscribe = [];
|
||||
var vm = this;
|
||||
|
||||
|
||||
const vm = this;
|
||||
|
||||
vm.loading = true;
|
||||
vm.model = $scope.model;
|
||||
vm.mediaEntry = vm.model.mediaEntry;
|
||||
vm.currentCrop = null;
|
||||
|
||||
localizationService.localizeMany([
|
||||
vm.model.createFlow ? "general_cancel" : "general_close",
|
||||
vm.model.createFlow ? "general_create" : "buttons_submitChanges"
|
||||
]).then(function (data) {
|
||||
vm.closeLabel = data[0];
|
||||
vm.submitLabel = data[1];
|
||||
});
|
||||
|
||||
vm.title = "";
|
||||
|
||||
vm.focalPointChanged = focalPointChanged;
|
||||
vm.onImageLoaded = onImageLoaded;
|
||||
vm.openMedia = openMedia;
|
||||
vm.repickMedia = repickMedia;
|
||||
vm.selectCrop = selectCrop;
|
||||
vm.deselectCrop = deselectCrop;
|
||||
vm.resetCrop = resetCrop;
|
||||
vm.submitAndClose = submitAndClose;
|
||||
vm.close = close;
|
||||
|
||||
function init() {
|
||||
|
||||
localizationService.localizeMany([
|
||||
vm.model.createFlow ? "general_cancel" : "general_close",
|
||||
vm.model.createFlow ? "general_create" : "buttons_submitChanges"
|
||||
]).then(data => {
|
||||
vm.closeLabel = data[0];
|
||||
vm.submitLabel = data[1];
|
||||
});
|
||||
|
||||
updateMedia();
|
||||
|
||||
unsubscribe.push(eventsService.on("editors.media.saved", function(name, args) {
|
||||
@@ -35,6 +45,7 @@ angular.module("umbraco")
|
||||
function updateMedia() {
|
||||
|
||||
vm.loading = true;
|
||||
|
||||
entityResource.getById(vm.mediaEntry.mediaKey, "Media").then(function (mediaEntity) {
|
||||
vm.media = mediaEntity;
|
||||
vm.imageSrc = mediaHelper.resolveFileFromEntity(mediaEntity, true);
|
||||
@@ -42,11 +53,11 @@ angular.module("umbraco")
|
||||
vm.hasDimensions = false;
|
||||
vm.isCroppable = false;
|
||||
|
||||
localizationService.localize("mediaPicker_editMediaEntryLabel", [vm.media.name, vm.model.documentName]).then(function (data) {
|
||||
localizationService.localize("mediaPicker_editMediaEntryLabel", [vm.media.name, vm.model.documentName]).then(data => {
|
||||
vm.title = data;
|
||||
});
|
||||
}, function () {
|
||||
localizationService.localize("mediaPicker_deletedItem").then(function (localized) {
|
||||
localizationService.localize("mediaPicker_deletedItem").then(localized => {
|
||||
vm.media = {
|
||||
name: localized,
|
||||
icon: "icon-picture",
|
||||
@@ -58,15 +69,12 @@ angular.module("umbraco")
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
vm.onImageLoaded = onImageLoaded;
|
||||
|
||||
function onImageLoaded(isCroppable, hasDimensions) {
|
||||
vm.isCroppable = isCroppable;
|
||||
vm.hasDimensions = hasDimensions;
|
||||
};
|
||||
|
||||
|
||||
vm.repickMedia = repickMedia;
|
||||
}
|
||||
|
||||
function repickMedia() {
|
||||
vm.model.propertyEditor.changeMediaFor(vm.model.mediaEntry, onMediaReplaced);
|
||||
}
|
||||
@@ -82,24 +90,23 @@ angular.module("umbraco")
|
||||
//
|
||||
updateMedia();
|
||||
}
|
||||
|
||||
vm.openMedia = openMedia;
|
||||
|
||||
function openMedia() {
|
||||
|
||||
var mediaEditor = {
|
||||
const mediaEditor = {
|
||||
id: vm.mediaEntry.mediaKey,
|
||||
submit: function () {
|
||||
submit: () => {
|
||||
editorService.close();
|
||||
},
|
||||
close: function () {
|
||||
close: () => {
|
||||
editorService.close();
|
||||
}
|
||||
};
|
||||
|
||||
editorService.mediaEditor(mediaEditor);
|
||||
}
|
||||
|
||||
|
||||
vm.focalPointChanged = function(left, top) {
|
||||
function focalPointChanged(left, top) {
|
||||
//update the model focalpoint value
|
||||
vm.mediaEntry.focalPoint = {
|
||||
left: left,
|
||||
@@ -109,22 +116,17 @@ angular.module("umbraco")
|
||||
//set form to dirty to track changes
|
||||
setDirty();
|
||||
}
|
||||
|
||||
|
||||
|
||||
vm.selectCrop = selectCrop;
|
||||
|
||||
function selectCrop(targetCrop) {
|
||||
vm.currentCrop = targetCrop;
|
||||
setDirty();
|
||||
// TODO: start watchin values of crop, first when changed set to dirty.
|
||||
};
|
||||
|
||||
vm.deselectCrop = deselectCrop;
|
||||
}
|
||||
|
||||
function deselectCrop() {
|
||||
vm.currentCrop = null;
|
||||
};
|
||||
|
||||
vm.resetCrop = resetCrop;
|
||||
}
|
||||
|
||||
function resetCrop() {
|
||||
if (vm.currentCrop) {
|
||||
$scope.$evalAsync( () => {
|
||||
@@ -138,18 +140,22 @@ angular.module("umbraco")
|
||||
vm.imageCropperForm.$setDirty();
|
||||
}
|
||||
|
||||
|
||||
vm.submitAndClose = function () {
|
||||
function submitAndClose() {
|
||||
if (vm.model && vm.model.submit) {
|
||||
vm.model.submit(vm.model);
|
||||
}
|
||||
}
|
||||
|
||||
vm.close = function () {
|
||||
if (vm.model && vm.model.close) {
|
||||
if (vm.model.createFlow === true || vm.imageCropperForm.$dirty === true) {
|
||||
var labels = vm.model.createFlow === true ? ["mediaPicker_confirmCancelMediaEntryCreationHeadline", "mediaPicker_confirmCancelMediaEntryCreationMessage"] : ["prompt_discardChanges", "mediaPicker_confirmCancelMediaEntryHasChanges"];
|
||||
localizationService.localizeMany(labels).then(function (localizations) {
|
||||
|
||||
function close() {
|
||||
if (vm.model && vm.model.close)
|
||||
{
|
||||
if (vm.model.createFlow === true || vm.imageCropperForm.$dirty === true)
|
||||
{
|
||||
const labelKeys = vm.model.createFlow === true
|
||||
? ["mediaPicker_confirmCancelMediaEntryCreationHeadline", "mediaPicker_confirmCancelMediaEntryCreationMessage"]
|
||||
: ["prompt_discardChanges", "mediaPicker_confirmCancelMediaEntryHasChanges"];
|
||||
|
||||
localizationService.localizeMany(labelKeys).then(localizations => {
|
||||
const confirm = {
|
||||
title: localizations[0],
|
||||
view: "default",
|
||||
@@ -157,11 +163,11 @@ angular.module("umbraco")
|
||||
submitButtonLabelKey: "general_discard",
|
||||
submitButtonStyle: "danger",
|
||||
closeButtonLabelKey: "prompt_stay",
|
||||
submit: function () {
|
||||
submit: () => {
|
||||
overlayService.close();
|
||||
vm.model.close(vm.model);
|
||||
},
|
||||
close: function () {
|
||||
close: () => {
|
||||
overlayService.close();
|
||||
}
|
||||
};
|
||||
@@ -175,6 +181,7 @@ angular.module("umbraco")
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
$scope.$on("$destroy", function () {
|
||||
unsubscribe.forEach(x => x());
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
data-element="property-{{property.alias}}"
|
||||
ng-repeat="property in tab.properties track by property.alias"
|
||||
property="property"
|
||||
show-inherit="propertyEditorDisabled(property)"
|
||||
show-inherit="propertyEditorInherited(property)"
|
||||
inherits-from="defaultVariant.displayName">
|
||||
|
||||
<div ng-class="{'o-40 cursor-not-allowed': propertyEditorDisabled(property) }">
|
||||
@@ -41,7 +41,7 @@
|
||||
data-element="property-{{property.alias}}"
|
||||
ng-repeat="property in group.properties track by property.alias"
|
||||
property="property"
|
||||
show-inherit="propertyEditorDisabled(property)"
|
||||
show-inherit="propertyEditorInherited(property)"
|
||||
inherits-from="defaultVariant.displayName">
|
||||
|
||||
<div ng-class="{'o-40 cursor-not-allowed': propertyEditorDisabled(property) }">
|
||||
|
||||
+2
-2
@@ -45,10 +45,10 @@
|
||||
show-inherit="vm.model.variants.length > 1 && !property.culture && !activeVariant.language.isDefault"
|
||||
inherits-from="defaultVariant.language.name">
|
||||
|
||||
<div ng-class="{'o-40 cursor-not-allowed': vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue}">
|
||||
<div ng-class="{'o-40 cursor-not-allowed': property.readonly || (vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue)}">
|
||||
<umb-property-editor
|
||||
model="property"
|
||||
preview="vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue">
|
||||
preview="property.readonly || (vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue)">
|
||||
</umb-property-editor>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<localize key="settingsDashboard_bulletPointTwo">Ask a question in the <a class="btn-link -underline" href="https://our.umbraco.com/forum" target="_blank" rel="noopener">Community Forum</a></localize>
|
||||
</li>
|
||||
<li>
|
||||
<localize key="settingsDashboard_bulletPointThree">Watch our <a class="btn-link -underline" href="https://umbraco.tv" target="_blank" rel="noopener">tutorial videos</a> (some are free, some require a subscription)</localize>
|
||||
<localize key="settingsDashboard_bulletPointTutorials">Watch our free <a class="btn-link -underline" href="https://umbra.co/ulb" target="_blank" rel="noopener">tutorial videos on the Umbraco Learning Base</a></localize>
|
||||
</li>
|
||||
<li>
|
||||
<localize key="settingsDashboard_bulletPointFour">Find out about our <a class="btn-link -underline" href="https://umbraco.com/products/" target="_blank" rel="noopener">productivity boosting tools and commercial support</a></localize>
|
||||
|
||||
@@ -667,6 +667,7 @@ angular.module("umbraco")
|
||||
$scope.removeControl = function (cell, $index) {
|
||||
$scope.currentControl = null;
|
||||
cell.controls.splice($index, 1);
|
||||
currentForm.$setDirty();
|
||||
};
|
||||
|
||||
$scope.percentage = function (spans) {
|
||||
|
||||
@@ -146,6 +146,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
|
||||
}
|
||||
|
||||
var listParamsForCurrent = $routeParams.id == $routeParams.list;
|
||||
|
||||
$scope.options = {
|
||||
useInfiniteEditor: $scope.model.config.useInfiniteEditor === true,
|
||||
pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10,
|
||||
@@ -169,6 +170,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
|
||||
allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete,
|
||||
cultureName: $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture
|
||||
};
|
||||
|
||||
_.each($scope.options.includeProperties, function (property) {
|
||||
property.nameExp = !!property.nameTemplate
|
||||
? $interpolate(property.nameTemplate)
|
||||
@@ -268,12 +270,13 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
|
||||
|
||||
$scope.getContent = function (contentId) {
|
||||
$scope.reloadView($scope.contentId, true);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.reloadView = function (id, reloadActiveNode) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.viewLoaded = false;
|
||||
$scope.folders = [];
|
||||
|
||||
@@ -327,7 +330,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
|
||||
$scope.options.pageNumber = 1;
|
||||
$scope.reloadView($scope.contentId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onSearchStartTyping = function() {
|
||||
$scope.viewLoaded = false;
|
||||
@@ -708,13 +711,15 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
|
||||
}
|
||||
|
||||
function initView() {
|
||||
|
||||
var id = $routeParams.id;
|
||||
if (id === undefined) {
|
||||
// no ID found in route params - don't list anything as we don't know for sure where we are
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.contentId = id;
|
||||
|
||||
// Get current id for node to load it's children
|
||||
$scope.contentId = editorState.current ? editorState.current.id : id;
|
||||
$scope.isTrashed = editorState.current ? editorState.current.trashed : id === "-20" || id === "-21";
|
||||
|
||||
$scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed;
|
||||
|
||||
@@ -348,9 +348,9 @@
|
||||
<WebProjectProperties>
|
||||
<UseIIS>False</UseIIS>
|
||||
<AutoAssignPort>True</AutoAssignPort>
|
||||
<DevelopmentServerPort>8180</DevelopmentServerPort>
|
||||
<DevelopmentServerPort>8183</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>http://localhost:8180</IISUrl>
|
||||
<IISUrl>http://localhost:8183</IISUrl>
|
||||
<NTLMAuthentication>False</NTLMAuthentication>
|
||||
<UseCustomServer>False</UseCustomServer>
|
||||
<CustomServerUrl>
|
||||
|
||||
@@ -1415,6 +1415,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
<key alias="theBestUmbracoVideoTutorials">The best Umbraco video tutorials</key>
|
||||
<key alias="umbracoForum">Visit our.umbraco.com</key>
|
||||
<key alias="umbracoTv">Visit umbraco.tv</key>
|
||||
<key alias="umbracoLearningBase">Watch our free tutorial videos</key>
|
||||
<key alias="umbracoLearningBaseDescription">on the Umbraco Learning Base</key>
|
||||
</area>
|
||||
<area alias="settings">
|
||||
<key alias="defaulttemplate">Default template</key>
|
||||
@@ -2506,9 +2508,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
Ask a question in the <a class="btn-link -underline" href="https://our.umbraco.com/forum" target="_blank" rel="noopener">Community Forum</a>
|
||||
]]>
|
||||
</key>
|
||||
<key alias="bulletPointThree">
|
||||
<key alias="bulletPointTutorials">
|
||||
<![CDATA[
|
||||
Watch our <a class="btn-link -underline" href="https://umbraco.tv" target="_blank" rel="noopener">tutorial videos</a> (some are free, some require a subscription)
|
||||
Watch our free <a class="btn-link -underline" href="https://umbra.co/ulb" target="_blank" rel="noopener">tutorial videos on the Umbraco Learning Base</a>
|
||||
]]>
|
||||
</key>
|
||||
<key alias="bulletPointFour">
|
||||
|
||||
@@ -1425,6 +1425,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
<key alias="theBestUmbracoVideoTutorials">The best Umbraco video tutorials</key>
|
||||
<key alias="umbracoForum">Visit our.umbraco.com</key>
|
||||
<key alias="umbracoTv">Visit umbraco.tv</key>
|
||||
<key alias="umbracoLearningBase">Watch our free tutorial videos</key>
|
||||
<key alias="umbracoLearningBaseDescription">on the Umbraco Learning Base</key>
|
||||
</area>
|
||||
<area alias="settings">
|
||||
<key alias="defaulttemplate">Default template</key>
|
||||
@@ -2544,9 +2546,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
Ask a question in the <a class="btn-link -underline" href="https://our.umbraco.com/forum" target="_blank" rel="noopener">Community Forum</a>
|
||||
]]>
|
||||
</key>
|
||||
<key alias="bulletPointThree">
|
||||
<key alias="bulletPointTutorials">
|
||||
<![CDATA[
|
||||
Watch our <a class="btn-link -underline" href="https://umbraco.tv" target="_blank" rel="noopener">tutorial videos</a> (some are free, some require a subscription)
|
||||
Watch our free <a class="btn-link -underline" href="https://umbra.co/ulb" target="_blank" rel="noopener">tutorial videos on the Umbraco Learning Base</a>
|
||||
]]>
|
||||
</key>
|
||||
<key alias="bulletPointFour">
|
||||
|
||||
@@ -306,7 +306,7 @@ namespace Umbraco.Web.Editors
|
||||
switch (type)
|
||||
{
|
||||
case Core.Constants.Trees.PartialViews:
|
||||
if (IsDirectory(virtualPath, SystemDirectories.PartialViews))
|
||||
if (IsDirectory(virtualPath, SystemDirectories.PartialViews, Current.FileSystems.PartialViewsFileSystem))
|
||||
{
|
||||
Services.FileService.DeletePartialViewFolder(virtualPath);
|
||||
return Request.CreateResponse(HttpStatusCode.OK);
|
||||
@@ -318,7 +318,7 @@ namespace Umbraco.Web.Editors
|
||||
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Partial View or folder found with the specified path");
|
||||
|
||||
case Core.Constants.Trees.PartialViewMacros:
|
||||
if (IsDirectory(virtualPath, SystemDirectories.MacroPartials))
|
||||
if (IsDirectory(virtualPath, SystemDirectories.MacroPartials, Current.FileSystems.MacroPartialsFileSystem))
|
||||
{
|
||||
Services.FileService.DeletePartialViewMacroFolder(virtualPath);
|
||||
return Request.CreateResponse(HttpStatusCode.OK);
|
||||
@@ -330,7 +330,7 @@ namespace Umbraco.Web.Editors
|
||||
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Partial View Macro or folder found with the specified path");
|
||||
|
||||
case Core.Constants.Trees.Scripts:
|
||||
if (IsDirectory(virtualPath, SystemDirectories.Scripts))
|
||||
if (IsDirectory(virtualPath, SystemDirectories.Scripts, Current.FileSystems.ScriptsFileSystem))
|
||||
{
|
||||
Services.FileService.DeleteScriptFolder(virtualPath);
|
||||
return Request.CreateResponse(HttpStatusCode.OK);
|
||||
@@ -343,7 +343,7 @@ namespace Umbraco.Web.Editors
|
||||
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Script or folder found with the specified path");
|
||||
|
||||
case Core.Constants.Trees.Stylesheets:
|
||||
if (IsDirectory(virtualPath, SystemDirectories.Css))
|
||||
if (IsDirectory(virtualPath, SystemDirectories.Css, Current.FileSystems.StylesheetsFileSystem))
|
||||
{
|
||||
Services.FileService.DeleteStyleSheetFolder(virtualPath);
|
||||
return Request.CreateResponse(HttpStatusCode.OK);
|
||||
@@ -634,14 +634,23 @@ namespace Umbraco.Web.Editors
|
||||
return value;
|
||||
}
|
||||
|
||||
private bool IsDirectory(string virtualPath, string systemDirectory)
|
||||
private bool IsDirectory(string virtualPath, string systemDirectory, IFileSystem fileSystem)
|
||||
{
|
||||
var path = IOHelper.MapPath(systemDirectory + "/" + virtualPath);
|
||||
var dirInfo = new DirectoryInfo(path);
|
||||
|
||||
// If you turn off indexing in Windows this will have the attribute:
|
||||
// `FileAttributes.Directory | FileAttributes.NotContentIndexed`
|
||||
return (dirInfo.Attributes & FileAttributes.Directory) != 0;
|
||||
// If it's a physical filesystem check with directory info
|
||||
if (fileSystem.CanAddPhysical)
|
||||
{
|
||||
var dirInfo = new DirectoryInfo(path);
|
||||
|
||||
// If you turn off indexing in Windows this will have the attribute:
|
||||
// `FileAttributes.Directory | FileAttributes.NotContentIndexed`
|
||||
return (dirInfo.Attributes & FileAttributes.Directory) != 0;
|
||||
}
|
||||
|
||||
// Otherwise check the filesystem abstraction to see if the folder exists
|
||||
// Since this is used for delete, it presumably exists if we're trying to delete it
|
||||
return fileSystem.DirectoryExists(path);
|
||||
}
|
||||
|
||||
// this is an internal class for passing stylesheet data from the client to the controller while editing
|
||||
|
||||
@@ -376,7 +376,7 @@ namespace Umbraco.Web.Editors
|
||||
[HttpPost]
|
||||
public IDictionary<string, ContentItemDisplay> GetEmptyByAliases(ContentTypesByAliases contentTypesByAliases)
|
||||
{
|
||||
// It's important to do this operation within a scope to reduce the amount of readlock queries.
|
||||
// It's important to do this operation within a scope to reduce the amount of readlock queries.
|
||||
using var scope = _scopeProvider.CreateScope(autoComplete: true);
|
||||
var contentTypes = contentTypesByAliases.ContentTypeAliases.Select(alias => Services.ContentTypeService.Get(alias));
|
||||
return GetEmpties(contentTypes, contentTypesByAliases.ParentId).ToDictionary(x => x.ContentTypeAlias);
|
||||
@@ -2019,6 +2019,8 @@ namespace Umbraco.Web.Editors
|
||||
|
||||
var variantIndex = 0;
|
||||
|
||||
var defaultCulture = _allLangs.Value.Values.FirstOrDefault(x => x.IsDefault)?.IsoCode;
|
||||
|
||||
//loop through each variant, set the correct name and property values
|
||||
foreach (var variant in contentSave.Variants)
|
||||
{
|
||||
@@ -2033,6 +2035,12 @@ namespace Umbraco.Web.Editors
|
||||
if (variant.Culture.IsNullOrWhiteSpace())
|
||||
throw new InvalidOperationException($"Cannot set culture name without a culture.");
|
||||
contentSave.PersistedContent.SetCultureName(variant.Name, variant.Culture);
|
||||
|
||||
// If the variant culture is the default culture we also want to update the name on the Content itself.
|
||||
if (variant.Culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
contentSave.PersistedContent.Name = variant.Name;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2072,6 +2080,12 @@ namespace Umbraco.Web.Editors
|
||||
variantIndex++;
|
||||
}
|
||||
|
||||
// Map IsDirty cultures to edited cultures, to make it easier to verify changes on specific variants on Saving and Saved events.
|
||||
var editedCultures = contentSave.PersistedContent.CultureInfos.Values
|
||||
.Where(x => x.IsDirty())
|
||||
.Select(x => x.Culture);
|
||||
contentSave.PersistedContent.SetCultureEdited(editedCultures);
|
||||
|
||||
// handle template
|
||||
if (string.IsNullOrWhiteSpace(contentSave.TemplateAlias)) // cleared: clear if not already null
|
||||
{
|
||||
@@ -2423,7 +2437,7 @@ namespace Umbraco.Web.Editors
|
||||
Items = results.Select(x => new ContentVersionMetaViewModel(x))
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[EnsureUserPermissionForContent("contentId", ActionUpdate.ActionLetter)]
|
||||
public HttpResponseMessage PostSetContentVersionPreventCleanup(int contentId, int versionId, bool preventCleanup)
|
||||
|
||||
@@ -20,6 +20,7 @@ using Umbraco.Core.Dashboards;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Web.Services;
|
||||
using System.Web.Http;
|
||||
using Umbraco.Core.Telemetry;
|
||||
|
||||
namespace Umbraco.Web.Editors
|
||||
{
|
||||
@@ -34,18 +35,28 @@ namespace Umbraco.Web.Editors
|
||||
{
|
||||
private readonly IDashboardService _dashboardService;
|
||||
private readonly IContentDashboardSettings _dashboardSettings;
|
||||
private readonly ISiteIdentifierService _siteIdentifierService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DashboardController"/> with all its dependencies.
|
||||
/// </summary>
|
||||
public DashboardController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor,
|
||||
ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger,
|
||||
IRuntimeState runtimeState, IDashboardService dashboardService, UmbracoHelper umbracoHelper,
|
||||
IContentDashboardSettings dashboardSettings)
|
||||
public DashboardController(
|
||||
IGlobalSettings globalSettings,
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
ISqlContext sqlContext,
|
||||
ServiceContext services,
|
||||
AppCaches appCaches,
|
||||
IProfilingLogger logger,
|
||||
IRuntimeState runtimeState,
|
||||
IDashboardService dashboardService,
|
||||
UmbracoHelper umbracoHelper,
|
||||
IContentDashboardSettings dashboardSettings,
|
||||
ISiteIdentifierService siteIdentifierService)
|
||||
: base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper)
|
||||
{
|
||||
_dashboardService = dashboardService;
|
||||
_dashboardSettings = dashboardSettings;
|
||||
_siteIdentifierService = siteIdentifierService;
|
||||
}
|
||||
|
||||
//we have just one instance of HttpClient shared for the entire application
|
||||
@@ -60,17 +71,19 @@ namespace Umbraco.Web.Editors
|
||||
var language = user.Language;
|
||||
var version = UmbracoVersion.SemanticVersion.ToSemanticString();
|
||||
var isAdmin = user.IsAdmin();
|
||||
_siteIdentifierService.TryGetOrCreateSiteIdentifier(out var siteIdentifier);
|
||||
|
||||
VerifyDashboardSource(baseUrl);
|
||||
|
||||
var url = string.Format("{0}{1}?section={2}&allowed={3}&lang={4}&version={5}&admin={6}",
|
||||
var url = string.Format("{0}{1}?section={2}&allowed={3}&lang={4}&version={5}&admin={6}&siteid={7}",
|
||||
baseUrl,
|
||||
_dashboardSettings.ContentDashboardPath,
|
||||
section,
|
||||
allowedSections,
|
||||
language,
|
||||
version,
|
||||
isAdmin);
|
||||
isAdmin,
|
||||
siteIdentifier.ToString());
|
||||
var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section;
|
||||
|
||||
var content = AppCaches.RuntimeCache.GetCacheItem<JObject>(key);
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Telemetry;
|
||||
using Umbraco.Web.Install.Models;
|
||||
|
||||
namespace Umbraco.Web.Install.InstallSteps
|
||||
@@ -14,54 +10,20 @@ namespace Umbraco.Web.Install.InstallSteps
|
||||
PerformsAppRestart = false)]
|
||||
internal class TelemetryIdentifierStep : InstallSetupStep<object>
|
||||
{
|
||||
private readonly IProfilingLogger _logger;
|
||||
private readonly IUmbracoSettingsSection _settings;
|
||||
private readonly ISiteIdentifierService _siteIdentifierService;
|
||||
|
||||
public TelemetryIdentifierStep(IProfilingLogger logger, IUmbracoSettingsSection settings)
|
||||
public TelemetryIdentifierStep(
|
||||
IUmbracoSettingsSection settings,
|
||||
ISiteIdentifierService siteIdentifierService)
|
||||
{
|
||||
_logger = logger;
|
||||
_settings = settings;
|
||||
_siteIdentifierService = siteIdentifierService;
|
||||
}
|
||||
|
||||
public override Task<InstallSetupResult> ExecuteAsync(object model)
|
||||
{
|
||||
// Generate GUID
|
||||
var telemetrySiteIdentifier = Guid.NewGuid();
|
||||
|
||||
// Modify the XML to add a new GUID site identifier
|
||||
// hack: ensure this does not trigger a restart
|
||||
using (ChangesMonitor.Suspended())
|
||||
{
|
||||
var umbracoSettingsPath = IOHelper.MapPath(SystemFiles.UmbracoSettings);
|
||||
if(File.Exists(umbracoSettingsPath) == false)
|
||||
{
|
||||
// Log an error
|
||||
_logger.Error<TelemetryIdentifierStep>("Unable to find umbracoSettings.config file to add telemetry site identifier");
|
||||
return Task.FromResult<InstallSetupResult>(null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var umbracoConfigXml = XDocument.Load(umbracoSettingsPath, LoadOptions.PreserveWhitespace);
|
||||
if (umbracoConfigXml.Root != null)
|
||||
{
|
||||
var backofficeElement = umbracoConfigXml.Root.Element("backOffice");
|
||||
if (backofficeElement == null)
|
||||
return Task.FromResult<InstallSetupResult>(null);
|
||||
|
||||
// Will add ID attribute if it does not exist
|
||||
backofficeElement.SetAttributeValue("id", telemetrySiteIdentifier.ToString());
|
||||
|
||||
// Save file back down
|
||||
umbracoConfigXml.Save(umbracoSettingsPath, SaveOptions.DisableFormatting);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error<TelemetryIdentifierStep>(ex, "Couldn't update umbracoSettings.config with a backoffice with a telemetry site identifier");
|
||||
}
|
||||
}
|
||||
|
||||
_siteIdentifierService.TryCreateSiteIdentifier(out _);
|
||||
return Task.FromResult<InstallSetupResult>(null);
|
||||
}
|
||||
|
||||
|
||||
@@ -87,8 +87,7 @@ namespace Umbraco.Web.Models.Mapping
|
||||
{
|
||||
MapSaveToTypeBase<DocumentTypeSave, PropertyTypeBasic>(source, target, context);
|
||||
MapComposition(source, target, alias => _contentTypeService.Get(alias));
|
||||
|
||||
target.HistoryCleanup = source.HistoryCleanup;
|
||||
MapHistoryCleanup(source, target);
|
||||
|
||||
target.AllowedTemplates = source.AllowedTemplates
|
||||
.Where(x => x != null)
|
||||
@@ -99,6 +98,34 @@ namespace Umbraco.Web.Models.Mapping
|
||||
target.SetDefaultTemplate(source.DefaultTemplate == null ? null : _fileService.GetTemplate(source.DefaultTemplate));
|
||||
}
|
||||
|
||||
private static void MapHistoryCleanup(DocumentTypeSave source, IContentType target)
|
||||
{
|
||||
// If source history cleanup is null we don't have to map all properties
|
||||
if (source.HistoryCleanup is null)
|
||||
{
|
||||
target.HistoryCleanup = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to reset the dirty properties, because it is otherwise true, just because the json serializer has set properties
|
||||
target.HistoryCleanup.ResetDirtyProperties(false);
|
||||
if (target.HistoryCleanup.PreventCleanup != source.HistoryCleanup.PreventCleanup)
|
||||
{
|
||||
target.HistoryCleanup.PreventCleanup = source.HistoryCleanup.PreventCleanup;
|
||||
}
|
||||
|
||||
if (target.HistoryCleanup.KeepAllVersionsNewerThanDays != source.HistoryCleanup.KeepAllVersionsNewerThanDays)
|
||||
{
|
||||
target.HistoryCleanup.KeepAllVersionsNewerThanDays = source.HistoryCleanup.KeepAllVersionsNewerThanDays;
|
||||
}
|
||||
|
||||
if (target.HistoryCleanup.KeepLatestVersionPerDayForDays !=
|
||||
source.HistoryCleanup.KeepLatestVersionPerDayForDays)
|
||||
{
|
||||
target.HistoryCleanup.KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays;
|
||||
}
|
||||
}
|
||||
|
||||
// no MapAll - take care
|
||||
private void Map(MediaTypeSave source, IMediaType target, MapperContext context)
|
||||
{
|
||||
@@ -253,7 +280,12 @@ namespace Umbraco.Web.Models.Mapping
|
||||
target.Id = source.Id;
|
||||
|
||||
if (source.GroupId > 0)
|
||||
target.PropertyGroupId = new Lazy<int>(() => source.GroupId, false);
|
||||
{
|
||||
if (target.PropertyGroupId?.Value != source.GroupId)
|
||||
{
|
||||
target.PropertyGroupId = new Lazy<int>(() => source.GroupId, false);
|
||||
}
|
||||
}
|
||||
|
||||
target.Alias = source.Alias;
|
||||
target.Description = source.Description;
|
||||
@@ -476,13 +508,23 @@ namespace Umbraco.Web.Models.Mapping
|
||||
|
||||
// ensure no duplicate alias, then assign the group properties collection
|
||||
EnsureUniqueAliases(destProperties);
|
||||
destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties);
|
||||
|
||||
if (destGroup.PropertyTypes.SupportsPublishing != isPublishing || destGroup.PropertyTypes.SequenceEqual(destProperties) is false)
|
||||
{
|
||||
destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties);
|
||||
}
|
||||
|
||||
destGroups.Add(destGroup);
|
||||
}
|
||||
|
||||
// ensure no duplicate name, then assign the groups collection
|
||||
EnsureUniqueAliases(destGroups);
|
||||
target.PropertyGroups = new PropertyGroupCollection(destGroups);
|
||||
|
||||
|
||||
if (target.PropertyGroups.SequenceEqual(destGroups) is false)
|
||||
{
|
||||
target.PropertyGroups = new PropertyGroupCollection(destGroups);
|
||||
}
|
||||
|
||||
// because the property groups collection was rebuilt, there is no need to remove
|
||||
// the old groups - they are just gone and will be cleared by the repository
|
||||
|
||||
@@ -433,10 +433,18 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
refreshedIdsA.Contains(x.ContentTypeId) &&
|
||||
BuildKit(x, out _)))
|
||||
{
|
||||
// replacing the node: must preserve the parents
|
||||
// replacing the node: must preserve the relations
|
||||
var node = GetHead(_contentNodes, kit.Node.Id)?.Value;
|
||||
if (node != null)
|
||||
{
|
||||
// Preserve children
|
||||
kit.Node.FirstChildContentId = node.FirstChildContentId;
|
||||
kit.Node.LastChildContentId = node.LastChildContentId;
|
||||
|
||||
// Also preserve siblings
|
||||
kit.Node.NextSiblingContentId = node.NextSiblingContentId;
|
||||
kit.Node.PreviousSiblingContentId = node.PreviousSiblingContentId;
|
||||
}
|
||||
|
||||
SetValueLocked(_contentNodes, kit.Node.Id, kit.Node);
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ using Umbraco.Web.WebApi;
|
||||
using Current = Umbraco.Web.Composing.Current;
|
||||
using Umbraco.Web.PropertyEditors;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Telemetry;
|
||||
using Umbraco.Web.Models;
|
||||
using Umbraco.Web.Telemetry;
|
||||
|
||||
namespace Umbraco.Web.Runtime
|
||||
{
|
||||
@@ -58,6 +60,8 @@ namespace Umbraco.Web.Runtime
|
||||
|
||||
composition.ComposeWebMappingProfiles();
|
||||
|
||||
composition.RegisterUnique<ISiteIdentifierService, SiteIdentifierService>();
|
||||
|
||||
//register the install components
|
||||
//NOTE: i tried to not have these registered if we weren't installing or upgrading but post install when the site restarts
|
||||
//it still needs to use the install controller so we can't do that
|
||||
|
||||
@@ -62,9 +62,6 @@ namespace Umbraco.Web.Telemetry
|
||||
{
|
||||
request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header
|
||||
|
||||
// Set a low timeout - no need to use a larger default timeout for this POST request
|
||||
_httpClient.Timeout = new TimeSpan(0, 0, 1);
|
||||
|
||||
// Make a HTTP Post to telemetry service
|
||||
// https://telemetry.umbraco.com/installs/
|
||||
// Fire & Forget, do not need to know if its a 200, 500 etc
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Xml.Linq;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Telemetry;
|
||||
using Umbraco.Web.Install;
|
||||
|
||||
namespace Umbraco.Web.Telemetry
|
||||
{
|
||||
internal class SiteIdentifierService : ISiteIdentifierService
|
||||
{
|
||||
private readonly IUmbracoSettingsSection _settings;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public SiteIdentifierService(IUmbracoSettingsSection settings, ILogger logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool TryGetSiteIdentifier(out Guid siteIdentifier)
|
||||
{
|
||||
// Parse telemetry string as a GUID & verify its a GUID and not some random string
|
||||
// since users may have messed with or decided to empty the app setting or put in something random
|
||||
if (Guid.TryParse(_settings.BackOffice.Id, out var parsedTelemetryId) is false
|
||||
|| parsedTelemetryId == Guid.Empty)
|
||||
{
|
||||
siteIdentifier = Guid.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
siteIdentifier = parsedTelemetryId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier)
|
||||
{
|
||||
if (TryGetSiteIdentifier(out var existingId))
|
||||
{
|
||||
siteIdentifier = existingId;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryCreateSiteIdentifier(out var createdId))
|
||||
{
|
||||
siteIdentifier = createdId;
|
||||
return true;
|
||||
}
|
||||
|
||||
siteIdentifier = Guid.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryCreateSiteIdentifier(out Guid createdGuid)
|
||||
{
|
||||
createdGuid = Guid.NewGuid();
|
||||
|
||||
// Modify the XML to add a new GUID site identifier
|
||||
// hack: ensure this does not trigger a restart
|
||||
using (ChangesMonitor.Suspended())
|
||||
{
|
||||
var umbracoSettingsPath = IOHelper.MapPath(SystemFiles.UmbracoSettings);
|
||||
|
||||
if (File.Exists(umbracoSettingsPath) is false)
|
||||
{
|
||||
_logger.Error<SiteIdentifierService>("Unable to find umbracoSettings.config file to add telemetry site identifier");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var umbracoConfigXml = XDocument.Load(umbracoSettingsPath, LoadOptions.PreserveWhitespace);
|
||||
if (umbracoConfigXml.Root != null)
|
||||
{
|
||||
var backofficeElement = umbracoConfigXml.Root.Element("backOffice");
|
||||
if (backofficeElement is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Will add ID attribute if it does not exist
|
||||
backofficeElement.SetAttributeValue("id", createdGuid.ToString());
|
||||
|
||||
// Save file back down
|
||||
umbracoConfigXml.Save(umbracoSettingsPath, SaveOptions.DisableFormatting);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error<SiteIdentifierService>(ex, "Couldn't update umbracoSettings.config with a backoffice with a telemetry site identifier");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace Umbraco.Web.Telemetry
|
||||
private readonly ITelemetryService _telemetryService;
|
||||
private BackgroundTaskRunner<IBackgroundTask> _telemetryReporterRunner;
|
||||
|
||||
public TelemetryComponent(IProfilingLogger logger, IUmbracoSettingsSection settings, ITelemetryService telemetryService)
|
||||
public TelemetryComponent(IProfilingLogger logger, ITelemetryService telemetryService)
|
||||
{
|
||||
_logger = logger;
|
||||
_telemetryService = telemetryService;
|
||||
|
||||
@@ -347,6 +347,7 @@
|
||||
<Compile Include="Models\LinkType.cs" />
|
||||
<Compile Include="Models\TemplateQuery\OperatorFactory.cs" />
|
||||
<Compile Include="Telemetry\ReportSiteTask.cs" />
|
||||
<Compile Include="Telemetry\SiteIdentifierService.cs" />
|
||||
<Compile Include="Telemetry\TelemetryComponent.cs" />
|
||||
<Compile Include="Telemetry\TelemetryComposer.cs" />
|
||||
<Compile Include="Templates\HtmlLocalLinkParser.cs" />
|
||||
|
||||
Reference in New Issue
Block a user