diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index c94ecb42f..bd183c66c 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -391,6 +391,8 @@ "contents.localizedFieldDescription": "The '{fieldName}' field of the content item (localized).", "contents.newStatusFieldDescription": "The new status of the content item.", "contents.noReference": "- No Reference -", + "contents.noReferences": "This content has no references.", + "contents.noReferencing": "This content is not referenced by another item.", "contents.pendingChangesTextToChange": "You have unsaved changes.\n\nWhen you change the status you will lose them.\n\n**Do you want to continue anyway?**", "contents.pendingChangesTextToClose": "You have unsaved changes.\n\nWhen you close the current content view you will lose them.\n\n**Do you want to continue anyway?**", "contents.pendingChangesTitle": "Unsaved changes", @@ -430,6 +432,9 @@ "contents.unpublishReferrerConfirmTitle": "Unpublish content", "contents.unsavedChangesText": "You have unsaved changes. Do you want to load them now?", "contents.unsavedChangesTitle": "Unsaved changes", + "contents.unsetValue": "Unset value", + "contents.unsetValueConfirmText": "If you unset the value you might loose your changes.\n\nDo you really want to do it?", + "contents.unsetValueConfirmTitle": "Do you want to unset the value?", "contents.updated": "Content updated successfully.", "contents.updateFailed": "Failed to update content. Please reload.", "contents.validate": "Validate", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 3ba2452f2..9b4eb8fbf 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -391,6 +391,8 @@ "contents.localizedFieldDescription": "Il campo '{fieldName}' del contenuto (localizzato).", "contents.newStatusFieldDescription": "Nuovo stato per l'elemento del contenuto.", "contents.noReference": "- Nessun collegamento -", + "contents.noReferences": "This content has no references.", + "contents.noReferencing": "This content is not referenced by another item.", "contents.pendingChangesTextToChange": "Non hai salvato le modifiche.\n\nSe cambi lo stato perderai le modifiche.\n\n**Sei sicuro di voler continuare?**", "contents.pendingChangesTextToClose": "Non hai salvato le modifiche.\n\nChiudendo il contenuto corrente perderai tutte le modifiche.\n\n**Sei sicuro di voler continuare?**", "contents.pendingChangesTitle": "Modifiche non salvate", @@ -430,6 +432,9 @@ "contents.unpublishReferrerConfirmTitle": "Unpublish content", "contents.unsavedChangesText": "Non hai salvato le modifiche. Vuoi salvarle adesso?", "contents.unsavedChangesTitle": "Modifiche non salvate", + "contents.unsetValue": "Unset value", + "contents.unsetValueConfirmText": "If you unset the value you might loose your changes.\n\nDo you really want to do it?", + "contents.unsetValueConfirmTitle": "Do you want to unset the value?", "contents.updated": "Contenuto aggiornato con successo.", "contents.updateFailed": "Non è stato possibile aggiornare il contenuto. Per favore ricarica.", "contents.validate": "Validate", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index b944fd00d..ec273f709 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -391,6 +391,8 @@ "contents.localizedFieldDescription": "Het veld '{fieldName}' van het inhoudsitem (gelokaliseerd).", "contents.newStatusFieldDescription": "De nieuwe status van het item.", "contents.noReference": "- Geen referentie -", + "contents.noReferences": "This content has no references.", + "contents.noReferencing": "This content is not referenced by another item.", "contents.pendingChangesTextToChange": "Je hebt niet-opgeslagen wijzigingen. \n \n Wanneer je de status wijzigt, raak je ze kwijt. \n \n **Wil je toch doorgaan?**", "contents.pendingChangesTextToClose": "Je hebt niet-opgeslagen wijzigingen. \n \n Wanneer je de huidige inhoudsweergave sluit, raak je ze kwijt. \n n **Wil je toch doorgaan?**", "contents.pendingChangesTitle": "Niet-opgeslagen wijzigingen", @@ -430,6 +432,9 @@ "contents.unpublishReferrerConfirmTitle": "Unpublish content", "contents.unsavedChangesText": "Je hebt niet-opgeslagen wijzigingen. Wil je ze nu laden?", "contents.unsavedChangesTitle": "Niet-opgeslagen wijzigingen", + "contents.unsetValue": "Unset value", + "contents.unsetValueConfirmText": "If you unset the value you might loose your changes.\n\nDo you really want to do it?", + "contents.unsetValueConfirmTitle": "Do you want to unset the value?", "contents.updated": "Inhoud succesvol bijgewerkt.", "contents.updateFailed": "Bijwerken van inhoud is mislukt. Laad opnieuw.", "contents.validate": "Validate", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index c94ecb42f..bd183c66c 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -391,6 +391,8 @@ "contents.localizedFieldDescription": "The '{fieldName}' field of the content item (localized).", "contents.newStatusFieldDescription": "The new status of the content item.", "contents.noReference": "- No Reference -", + "contents.noReferences": "This content has no references.", + "contents.noReferencing": "This content is not referenced by another item.", "contents.pendingChangesTextToChange": "You have unsaved changes.\n\nWhen you change the status you will lose them.\n\n**Do you want to continue anyway?**", "contents.pendingChangesTextToClose": "You have unsaved changes.\n\nWhen you close the current content view you will lose them.\n\n**Do you want to continue anyway?**", "contents.pendingChangesTitle": "Unsaved changes", @@ -430,6 +432,9 @@ "contents.unpublishReferrerConfirmTitle": "Unpublish content", "contents.unsavedChangesText": "You have unsaved changes. Do you want to load them now?", "contents.unsavedChangesTitle": "Unsaved changes", + "contents.unsetValue": "Unset value", + "contents.unsetValueConfirmText": "If you unset the value you might loose your changes.\n\nDo you really want to do it?", + "contents.unsetValueConfirmTitle": "Do you want to unset the value?", "contents.updated": "Content updated successfully.", "contents.updateFailed": "Failed to update content. Please reload.", "contents.validate": "Validate", diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs index 622b45b33..33ba1f149 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs @@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public string EventsFilter { - get { return "^assetFolder\\-"; } + get { return "^assetFolder-"; } } public RecursiveDeleter( diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetUsageTrackerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetUsageTrackerTests.cs new file mode 100644 index 000000000..241d0e1e6 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetUsageTrackerTests.cs @@ -0,0 +1,160 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using NodaTime; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.UsageTracking; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetUsageTrackerTests + { + private readonly IUsageTracker usageTracker = A.Fake(); + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly AssetUsageTracker sut; + + public AssetUsageTrackerTests() + { + sut = new AssetUsageTracker(usageTracker); + } + + [Fact] + public void Should_return_assets_filter_for_events_filter() + { + IEventConsumer consumer = sut; + + Assert.Equal("^asset-", consumer.EventsFilter); + } + + [Fact] + public async Task Should_do_nothing_on_clear() + { + IEventConsumer consumer = sut; + + await consumer.ClearAsync(); + } + + [Fact] + public void Should_return_type_name_for_name() + { + IEventConsumer consumer = sut; + + Assert.Equal(nameof(AssetUsageTracker), consumer.Name); + } + + [Fact] + public async Task Should_get_total_size_from_summary_date() + { + A.CallTo(() => usageTracker.GetAsync($"{appId.Id}_Assets", default, default, null)) + .Returns(new Counters { ["TotalSize"] = 2048 }); + + var size = await sut.GetTotalSizeAsync(appId.Id); + + Assert.Equal(2048, size); + } + + [Theory] + [InlineData("*")] + [InlineData("Default")] + public async Task Should_get_counters_from_categories(string category) + { + var dateFrom = new DateTime(2018, 01, 05); + var dateTo = dateFrom.AddDays(3); + + A.CallTo(() => usageTracker.QueryAsync($"{appId.Id}_Assets", dateFrom, dateTo)) + .Returns(new Dictionary> + { + [category] = new List<(DateTime, Counters)> + { + (dateFrom.AddDays(0), new Counters + { + ["TotalSize"] = 128, + ["TotalAssets"] = 2, + }), + (dateFrom.AddDays(1), new Counters + { + ["TotalSize"] = 256, + ["TotalAssets"] = 3, + }), + (dateFrom.AddDays(2), new Counters + { + ["TotalSize"] = 512, + ["TotalAssets"] = 4, + }) + } + }); + + var result = await sut.QueryAsync(appId.Id, dateFrom, dateTo); + + result.Should().BeEquivalentTo(new List + { + new AssetStats(dateFrom.AddDays(0), 2, 128), + new AssetStats(dateFrom.AddDays(1), 3, 256), + new AssetStats(dateFrom.AddDays(2), 4, 512), + }); + } + + public static IEnumerable EventData() + { + yield return new object[] + { + new AssetCreated { FileSize = 128 }, 128, 1 + }; + + yield return new object[] + { + new AssetUpdated { FileSize = 512 }, 512, 0 + }; + + yield return new object[] + { + new AssetDeleted { DeletedSize = 512 }, -512, -1 + }; + } + + [Theory] + [MemberData(nameof(EventData))] + public async Task Should_increase_usage_when_asset_created(AssetEvent @event, long sizeDiff, long countDiff) + { + var date = DateTime.UtcNow.Date.AddDays(13); + + @event.AppId = appId; + + var envelope = + Envelope.Create(@event) + .SetTimestamp(Instant.FromDateTimeUtc(date)); + + Counters? countersSummary = null; + Counters? countersDate = null; + + A.CallTo(() => usageTracker.TrackAsync(default, $"{appId.Id}_Assets", null, A._)) + .Invokes(x => countersSummary = x.GetArgument(3)); + + A.CallTo(() => usageTracker.TrackAsync(date, $"{appId.Id}_Assets", null, A._)) + .Invokes(x => countersDate = x.GetArgument(3)); + + await sut.On(envelope); + + var expected = new Counters + { + ["TotalSize"] = sizeDiff, + ["TotalAssets"] = countDiff + }; + + countersSummary.Should().BeEquivalentTo(expected); + countersDate.Should().BeEquivalentTo(expected); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs index d0e049e70..b448f8d86 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs @@ -18,7 +18,7 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { - public class AssetFolderDomainObjectTests : HandlerTestBase + public class AssetFolderDomainObjectTests : HandlerTestBase { private readonly IAssetQueryService assetQuery = A.Fake(); private readonly DomainId parentId = DomainId.NewGuid(); diff --git a/frontend/app/features/content/pages/content/editor/field-languages.component.html b/frontend/app/features/content/pages/content/editor/field-languages.component.html index 021654828..24533aba2 100644 --- a/frontend/app/features/content/pages/content/editor/field-languages.component.html +++ b/frontend/app/features/content/pages/content/editor/field-languages.component.html @@ -10,11 +10,13 @@ - - +
+ + +
{{ 'contents.validationHint' | sqxTranslate }} diff --git a/frontend/app/features/content/pages/content/editor/field-languages.component.scss b/frontend/app/features/content/pages/content/editor/field-languages.component.scss index e69de29bb..eb812816d 100644 --- a/frontend/app/features/content/pages/content/editor/field-languages.component.scss +++ b/frontend/app/features/content/pages/content/editor/field-languages.component.scss @@ -0,0 +1,9 @@ +.button-container { + display: inline-block; + max-width: none; + min-width: 5rem; +} + +sqx-language-selector { + text-align: right; +} \ No newline at end of file diff --git a/frontend/app/features/content/pages/content/references/content-references.component.html b/frontend/app/features/content/pages/content/references/content-references.component.html index 052e1f355..ab44e2ac2 100644 --- a/frontend/app/features/content/pages/content/references/content-references.component.html +++ b/frontend/app/features/content/pages/content/references/content-references.component.html @@ -1,16 +1,27 @@ - - + + + + + + + +
+ {{ 'i18n:contents.noReferences' | sqxTranslate }} + + {{ 'i18n:contents.noReferencing' | sqxTranslate }} +
diff --git a/frontend/app/features/content/shared/forms/field-editor.component.html b/frontend/app/features/content/shared/forms/field-editor.component.html index d1c0d0231..37c13a742 100644 --- a/frontend/app/features/content/shared/forms/field-editor.component.html +++ b/frontend/app/features/content/shared/forms/field-editor.component.html @@ -176,8 +176,12 @@ -
-
diff --git a/frontend/app/framework/angular/language-selector.component.scss b/frontend/app/framework/angular/language-selector.component.scss index 09c19bbc9..11a2b2fe5 100644 --- a/frontend/app/framework/angular/language-selector.component.scss +++ b/frontend/app/framework/angular/language-selector.component.scss @@ -2,10 +2,6 @@ cursor: pointer; } -.dropdown-toggle { - min-width: 5rem; -} - .iso-code { font-family: monospace; }