diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/ByKeyDictionaryController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/ByKeyDictionaryController.cs index a595296dc3..4e1ece8db3 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/ByKeyDictionaryController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/ByKeyDictionaryController.cs @@ -1,40 +1,28 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Api.Management.ViewModels.Dictionary; -using Umbraco.New.Cms.Core.Factories; namespace Umbraco.Cms.Api.Management.Controllers.Dictionary; -public class ByIdDictionaryController : DictionaryControllerBase +public class ByKeyDictionaryController : DictionaryControllerBase { private readonly ILocalizationService _localizationService; private readonly IDictionaryFactory _dictionaryFactory; - public ByIdDictionaryController( - ILocalizationService localizationService, - IDictionaryFactory dictionaryFactory) + public ByKeyDictionaryController(ILocalizationService localizationService, IDictionaryFactory dictionaryFactory) { _localizationService = localizationService; _dictionaryFactory = dictionaryFactory; } - /// - /// Gets a dictionary item by guid - /// - /// - /// The id. - /// - /// - /// The . Returns a not found response when dictionary item does not exist - /// - [HttpGet("{key:guid}")] + [HttpGet($"{{{nameof(key)}:guid}}")] [MapToApiVersion("1.0")] - [ProducesResponseType(typeof(DictionaryViewModel), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)] - public async Task> ByKey(Guid key) + [ProducesResponseType(typeof(DictionaryItemViewModel), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> ByKey(Guid key) { IDictionaryItem? dictionary = _localizationService.GetDictionaryItemById(key); if (dictionary == null) @@ -42,6 +30,6 @@ public class ByIdDictionaryController : DictionaryControllerBase return NotFound(); } - return await Task.FromResult(_dictionaryFactory.CreateDictionaryViewModel(dictionary)); + return await Task.FromResult(Ok(_dictionaryFactory.CreateDictionaryItemViewModel(dictionary))); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/CreateDictionaryController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/CreateDictionaryController.cs index ab0cc69625..76e78f4f83 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/CreateDictionaryController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/CreateDictionaryController.cs @@ -1,90 +1,52 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Api.Management.ViewModels.Dictionary; -using Umbraco.Extensions; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Api.Management.Controllers.Dictionary; public class CreateDictionaryController : DictionaryControllerBase { private readonly ILocalizationService _localizationService; - private readonly ILocalizedTextService _localizedTextService; - private readonly GlobalSettings _globalSettings; - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; - private readonly ILogger _logger; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IDictionaryFactory _dictionaryFactory; public CreateDictionaryController( ILocalizationService localizationService, - ILocalizedTextService localizedTextService, - IOptionsSnapshot globalSettings, - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - ILogger logger) + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IDictionaryFactory dictionaryFactory) { _localizationService = localizationService; - _localizedTextService = localizedTextService; - _globalSettings = globalSettings.Value; - _backofficeSecurityAccessor = backofficeSecurityAccessor; - _logger = logger; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _dictionaryFactory = dictionaryFactory; } - /// - /// Creates a new dictionary item - /// - /// The viewmodel to pass to the action - /// - /// The . - /// [HttpPost] [MapToApiVersion("1.0")] - [ProducesResponseType(typeof(CreatedResult), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] - public async Task> Create(DictionaryItemViewModel dictionaryViewModel) + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] + public async Task Create(DictionaryItemCreateModel dictionaryItemCreateModel) { - if (string.IsNullOrEmpty(dictionaryViewModel.Key.ToString())) + IEnumerable translations = _dictionaryFactory.MapTranslations(dictionaryItemCreateModel.Translations); + + Attempt result = _localizationService.Create( + dictionaryItemCreateModel.Name, + dictionaryItemCreateModel.ParentKey, + translations, + CurrentUserId(_backOfficeSecurityAccessor)); + + if (result.Success) { - return ValidationProblem("Key can not be empty."); // TODO: translate + return await Task.FromResult(CreatedAtAction(controller => nameof(controller.ByKey), result.Result!.Key)); } - if (_localizationService.DictionaryItemExists(dictionaryViewModel.Key.ToString())) - { - var message = _localizedTextService.Localize( - "dictionaryItem", - "changeKeyError", - _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.GetUserCulture(_localizedTextService, _globalSettings), - new Dictionary - { - { "0", dictionaryViewModel.Key.ToString() }, - }); - return await Task.FromResult(ValidationProblem(message)); - } - - try - { - Guid? parentGuid = null; - - if (dictionaryViewModel.ParentId.HasValue) - { - parentGuid = dictionaryViewModel.ParentId; - } - - IDictionaryItem item = _localizationService.CreateDictionaryItemWithIdentity( - dictionaryViewModel.Key.ToString(), - parentGuid, - string.Empty); - - - return await Task.FromResult(Created($"api/v1.0/dictionary/{item.Key}", item.Key)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating dictionary with {Name} under {ParentId}", dictionaryViewModel.Key, dictionaryViewModel.ParentId); - return await Task.FromResult(ValidationProblem("Error creating dictionary item")); - } + return DictionaryItemOperationStatusResult(result.Status); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/DeleteDictionaryController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/DeleteDictionaryController.cs index 0f86cc0e5a..7e86c5743a 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/DeleteDictionaryController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/DeleteDictionaryController.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Api.Management.Controllers.Dictionary; @@ -16,36 +18,20 @@ public class DeleteDictionaryController : DictionaryControllerBase _localizationService = localizationService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } - /// - /// Deletes a data type with a given ID - /// - /// The key of the dictionary item to delete - /// - /// - /// - [HttpDelete("{key}")] + + [HttpDelete($"{{{nameof(key)}:guid}}")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task Delete(Guid key) { - IDictionaryItem? foundDictionary = _localizationService.GetDictionaryItemByKey(key.ToString()); - - if (foundDictionary == null) + Attempt result = _localizationService.Delete(key, CurrentUserId(_backOfficeSecurityAccessor)); + if (result.Success) { - return await Task.FromResult(NotFound()); + return await Task.FromResult(Ok()); } - IEnumerable foundDictionaryDescendants = - _localizationService.GetDictionaryItemDescendants(foundDictionary.Key); - - foreach (IDictionaryItem dictionaryItem in foundDictionaryDescendants) - { - _localizationService.Delete(dictionaryItem, _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); - } - - _localizationService.Delete(foundDictionary, _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); - - return await Task.FromResult(Ok()); + return DictionaryItemOperationStatusResult(result.Status); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/DictionaryControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/DictionaryControllerBase.cs index 4914298d14..eb66fa3153 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/DictionaryControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/DictionaryControllerBase.cs @@ -1,5 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Builders; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Api.Management.Controllers.Dictionary; @@ -10,4 +13,19 @@ namespace Umbraco.Cms.Api.Management.Controllers.Dictionary; // TODO: Add authentication public abstract class DictionaryControllerBase : ManagementApiControllerBase { + protected IActionResult DictionaryItemOperationStatusResult(DictionaryItemOperationStatus status) => + status switch + { + DictionaryItemOperationStatus.DuplicateItemKey => Conflict(new ProblemDetailsBuilder() + .WithTitle("Duplicate dictionary item name detected") + .WithDetail("Another dictionary item exists with the same name. Dictionary item names must be unique.") + .Build()), + DictionaryItemOperationStatus.ItemNotFound => NotFound("The dictionary item could not be found"), + DictionaryItemOperationStatus.ParentNotFound => NotFound("The dictionary item parent could not be found"), + DictionaryItemOperationStatus.CancelledByNotification => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Cancelled by notification") + .WithDetail("A notification handler prevented the dictionary item operation.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown dictionary operation status") + }; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/DictionaryTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/DictionaryTreeControllerBase.cs index 87fdd2bdde..b2641ddf8d 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/DictionaryTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/DictionaryTreeControllerBase.cs @@ -32,6 +32,7 @@ public class DictionaryTreeControllerBase : EntityTreeControllerBase Update(Guid id, JsonPatchViewModel[] updateViewModel) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Update(Guid key, DictionaryItemUpdateModel dictionaryItemUpdateModel) { - IDictionaryItem? dictionaryItem = _localizationService.GetDictionaryItemById(id); - - if (dictionaryItem is null) + IDictionaryItem? current = _localizationService.GetDictionaryItemById(key); + if (current == null) { return NotFound(); } - DictionaryViewModel dictionaryToPatch = _umbracoMapper.Map(dictionaryItem)!; + IDictionaryItem updated = _dictionaryFactory.MapUpdateModelToDictionaryItem(current, dictionaryItemUpdateModel); - PatchResult? result = _jsonPatchService.Patch(updateViewModel, dictionaryToPatch); + Attempt result = _localizationService.Update(updated, CurrentUserId(_backOfficeSecurityAccessor)); - if (result?.Result is null) + if (result.Success) { - throw new JsonException("Could not patch the JsonPatchViewModel"); + return await Task.FromResult(Ok()); } - DictionaryViewModel? updatedDictionaryItem = _systemTextJsonSerializer.Deserialize(result.Result.ToJsonString()); - if (updatedDictionaryItem is null) - { - throw new JsonException("Could not serialize from PatchResult to DictionaryViewModel"); - } - - IDictionaryItem dictionaryToSave = _dictionaryFactory.CreateDictionaryItem(updatedDictionaryItem!); - _localizationService.Save(dictionaryToSave); - return await Task.FromResult(Content(_dictionaryService.CalculatePath(dictionaryToSave.ParentId, dictionaryToSave.Id), MediaTypeNames.Text.Plain, Encoding.UTF8)); + return DictionaryItemOperationStatusResult(result.Status); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/UploadDictionaryController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/UploadDictionaryController.cs index 32156d21c0..e23a2c1566 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/UploadDictionaryController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/UploadDictionaryController.cs @@ -1,6 +1,7 @@ using System.Xml; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Api.Management.Models; using Umbraco.Cms.Api.Management.Services; diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/DictionaryBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/DictionaryBuilderExtensions.cs index eee1bcd56a..059b8411c7 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/DictionaryBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/DictionaryBuilderExtensions.cs @@ -4,7 +4,6 @@ using Umbraco.Cms.Api.Management.Mapping.Dictionary; using Umbraco.Cms.Api.Management.Services; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; -using Umbraco.New.Cms.Core.Factories; namespace Umbraco.Cms.Api.Management.DependencyInjection; @@ -16,7 +15,7 @@ internal static class DictionaryBuilderExtensions .AddTransient() .AddTransient(); - builder.WithCollectionBuilder().Add(); + builder.WithCollectionBuilder().Add(); return builder; } diff --git a/src/Umbraco.Cms.Api.Management/Factories/DictionaryFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DictionaryFactory.cs index 651a806244..bf629e3139 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DictionaryFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DictionaryFactory.cs @@ -1,11 +1,10 @@ using System.Xml; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Mapping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Api.Management.Models; using Umbraco.Cms.Api.Management.ViewModels.Dictionary; -using Umbraco.New.Cms.Core.Factories; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Factories; @@ -13,59 +12,50 @@ public class DictionaryFactory : IDictionaryFactory { private readonly IUmbracoMapper _umbracoMapper; private readonly ILocalizationService _localizationService; - private readonly IDictionaryService _dictionaryService; - private readonly CommonMapper _commonMapper; - public DictionaryFactory( - IUmbracoMapper umbracoMapper, - ILocalizationService localizationService, - IDictionaryService dictionaryService, - CommonMapper commonMapper) + public DictionaryFactory(IUmbracoMapper umbracoMapper, ILocalizationService localizationService) { _umbracoMapper = umbracoMapper; _localizationService = localizationService; - _dictionaryService = dictionaryService; - _commonMapper = commonMapper; } - public IDictionaryItem CreateDictionaryItem(DictionaryViewModel dictionaryViewModel) + public DictionaryItemViewModel CreateDictionaryItemViewModel(IDictionaryItem dictionaryItem) { - IDictionaryItem mappedItem = _umbracoMapper.Map(dictionaryViewModel)!; - IDictionaryItem? dictionaryItem = _localizationService.GetDictionaryItemById(dictionaryViewModel.Key); - mappedItem.Id = dictionaryItem!.Id; - return mappedItem; - } + DictionaryItemViewModel dictionaryViewModel = _umbracoMapper.Map(dictionaryItem)!; - public DictionaryViewModel CreateDictionaryViewModel(IDictionaryItem dictionaryItem) - { - DictionaryViewModel dictionaryViewModel = _umbracoMapper.Map(dictionaryItem)!; - - dictionaryViewModel.ContentApps = _commonMapper.GetContentAppsForEntity(dictionaryItem); - dictionaryViewModel.Path = _dictionaryService.CalculatePath(dictionaryItem.ParentId, dictionaryItem.Id); - - var translations = new List(); - // add all languages and the translations - foreach (ILanguage lang in _localizationService.GetAllLanguages()) - { - var langId = lang.Id; - IDictionaryTranslation? translation = dictionaryItem.Translations?.FirstOrDefault(x => x.LanguageId == langId); - - translations.Add(new DictionaryTranslationViewModel - { - IsoCode = lang.IsoCode, - DisplayName = lang.CultureName, - Translation = translation?.Value ?? string.Empty, - LanguageId = lang.Id, - Id = translation?.Id ?? 0, - Key = translation?.Key ?? Guid.Empty, - }); - } - - dictionaryViewModel.Translations = translations; + var validLanguageIds = _localizationService + .GetAllLanguages() + .Select(language => language.Id) + .ToArray(); + IDictionaryTranslation[] validTranslations = dictionaryItem.Translations + .Where(t => validLanguageIds.Contains(t.LanguageId)) + .ToArray(); + dictionaryViewModel.Translations = validTranslations + .Select(translation => _umbracoMapper.Map(translation)) + .WhereNotNull() + .ToArray(); return dictionaryViewModel; } + public IDictionaryItem MapUpdateModelToDictionaryItem(IDictionaryItem current, DictionaryItemUpdateModel dictionaryItemUpdateModel) + { + IDictionaryItem updated = _umbracoMapper.Map(dictionaryItemUpdateModel, current); + + MapTranslations(updated, dictionaryItemUpdateModel.Translations); + + return updated; + } + + public IEnumerable MapTranslations(IEnumerable translationModels) + { + var temporaryDictionaryItem = new DictionaryItem(Guid.NewGuid().ToString()); + + MapTranslations(temporaryDictionaryItem, translationModels); + + return temporaryDictionaryItem.Translations; + } + public DictionaryImportViewModel CreateDictionaryImportViewModel(FormFileUploadResult formFileUploadResult) { if (formFileUploadResult.CouldLoad is false || formFileUploadResult.XmlDocument is null) @@ -83,7 +73,7 @@ public class DictionaryFactory : IDictionaryFactory foreach (XmlNode dictionaryItem in formFileUploadResult.XmlDocument.GetElementsByTagName("DictionaryItem")) { var name = dictionaryItem.Attributes?.GetNamedItem("Name")?.Value ?? string.Empty; - var parentKey = dictionaryItem?.ParentNode?.Attributes?.GetNamedItem("Key")?.Value ?? string.Empty; + var parentKey = dictionaryItem.ParentNode?.Attributes?.GetNamedItem("Key")?.Value ?? string.Empty; if (parentKey != currentParent || level == 1) { @@ -96,4 +86,19 @@ public class DictionaryFactory : IDictionaryFactory return model; } + + private void MapTranslations(IDictionaryItem dictionaryItem, IEnumerable translationModels) + { + var languagesByIsoCode = _localizationService + .GetAllLanguages() + .ToDictionary(l => l.IsoCode); + DictionaryItemTranslationModel[] validTranslations = translationModels + .Where(translation => languagesByIsoCode.ContainsKey(translation.IsoCode)) + .ToArray(); + + foreach (DictionaryItemTranslationModel translationModel in validTranslations) + { + _localizationService.AddOrUpdateDictionaryValue(dictionaryItem, languagesByIsoCode[translationModel.IsoCode], translationModel.Translation); + } + } } diff --git a/src/Umbraco.Cms.Api.Management/Factories/IDictionaryFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IDictionaryFactory.cs index fb86b6aec0..209f9c0981 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IDictionaryFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IDictionaryFactory.cs @@ -1,13 +1,16 @@ -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Api.Management.Models; +using Umbraco.Cms.Api.Management.Models; using Umbraco.Cms.Api.Management.ViewModels.Dictionary; +using Umbraco.Cms.Core.Models; -namespace Umbraco.New.Cms.Core.Factories; +namespace Umbraco.Cms.Api.Management.Factories; public interface IDictionaryFactory { - IDictionaryItem CreateDictionaryItem(DictionaryViewModel dictionaryViewModel); - DictionaryViewModel CreateDictionaryViewModel(IDictionaryItem dictionaryItem); + IDictionaryItem MapUpdateModelToDictionaryItem(IDictionaryItem current, DictionaryItemUpdateModel dictionaryItemUpdateModel); + + IEnumerable MapTranslations(IEnumerable translationModels); + + DictionaryItemViewModel CreateDictionaryItemViewModel(IDictionaryItem dictionaryItem); DictionaryImportViewModel CreateDictionaryImportViewModel(FormFileUploadResult formFileUploadResult); } diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Dictionary/DictionaryMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Dictionary/DictionaryMapDefinition.cs new file mode 100644 index 0000000000..7c19b1d877 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/Dictionary/DictionaryMapDefinition.cs @@ -0,0 +1,72 @@ +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Api.Management.ViewModels.Dictionary; + +namespace Umbraco.Cms.Api.Management.Mapping.Dictionary; + +public class DictionaryMapDefinition : IMapDefinition +{ + private readonly ILocalizationService _localizationService; + + public DictionaryMapDefinition(ILocalizationService localizationService) => _localizationService = localizationService; + + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((_, _) => new DictionaryItemViewModel(), Map); + mapper.Define((_, _) => new DictionaryItemTranslationModel(), Map); + mapper.Define((_, _) => new DictionaryItem(string.Empty), Map); + mapper.Define((_, _) => new DictionaryItem(string.Empty), Map); + mapper.Define((_, _) => new DictionaryOverviewViewModel(), Map); + } + + // Umbraco.Code.MapAll -Translations + private void Map(IDictionaryItem source, DictionaryItemViewModel target, MapperContext context) + { + target.Key = source.Key; + target.Name = source.ItemKey; + } + + // Umbraco.Code.MapAll + private void Map(IDictionaryTranslation source, DictionaryItemTranslationModel target, MapperContext context) + { + target.IsoCode = source.Language?.IsoCode ?? throw new ArgumentException("Translation has no language", nameof(source)); + target.Translation = source.Value; + } + + // Umbraco.Code.MapAll -Id -Key -CreateDate -UpdateDate -ParentId -Translations + private void Map(DictionaryItemUpdateModel source, IDictionaryItem target, MapperContext context) + { + target.ItemKey = source.Name; + target.DeleteDate = null; + } + + // Umbraco.Code.MapAll -Id -Key -CreateDate -UpdateDate -Translations + private void Map(DictionaryItemCreateModel source, IDictionaryItem target, MapperContext context) + { + target.ItemKey = source.Name; + target.ParentId = source.ParentKey; + target.DeleteDate = null; + } + + // Umbraco.Code.MapAll -Level -Translations + private void Map(IDictionaryItem source, DictionaryOverviewViewModel target, MapperContext context) + { + target.Key = source.Key; + target.Name = source.ItemKey; + + // add all languages and the translations + foreach (ILanguage lang in _localizationService.GetAllLanguages()) + { + var langId = lang.Id; + IDictionaryTranslation? translation = source.Translations?.FirstOrDefault(x => x.LanguageId == langId); + + target.Translations.Add( + new DictionaryTranslationOverviewViewModel + { + DisplayName = lang.CultureName, + HasTranslation = translation != null && string.IsNullOrEmpty(translation.Value) == false, + }); + } + } +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Dictionary/DictionaryViewModelMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Dictionary/DictionaryViewModelMapDefinition.cs deleted file mode 100644 index f0721bb032..0000000000 --- a/src/Umbraco.Cms.Api.Management/Mapping/Dictionary/DictionaryViewModelMapDefinition.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Mapping; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.ContentEditing; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Api.Management.ViewModels.Dictionary; - -namespace Umbraco.Cms.Api.Management.Mapping.Dictionary; - -public class DictionaryViewModelMapDefinition : IMapDefinition -{ - private readonly ILocalizationService _localizationService; - - public DictionaryViewModelMapDefinition(ILocalizationService localizationService) => _localizationService = localizationService; - - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new DictionaryItem(string.Empty), Map); - mapper.Define((source, context) => new DictionaryViewModel(), Map); - mapper.Define((source, context) => new DictionaryTranslation(source.LanguageId, string.Empty), Map); - mapper.Define((source, context) => new DictionaryOverviewViewModel(), Map); - - } - - // Umbraco.Code.MapAll -Id -CreateDate -UpdateDate - private void Map(DictionaryViewModel source, IDictionaryItem target, MapperContext context) - { - target.ItemKey = source.Name!; - target.Key = source.Key; - target.ParentId = source.ParentId; - target.Translations = context.MapEnumerable(source.Translations); - target.DeleteDate = null; - } - - // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate -Language - private void Map(DictionaryTranslationViewModel source, IDictionaryTranslation target, MapperContext context) - { - target.Value = source.Translation; - target.Id = source.Id; - target.Key = source.Key; - } - - // Umbraco.Code.MapAll -Icon -Trashed -Alias -NameIsDirty -ContentApps -Path -Translations - private void Map(IDictionaryItem source, DictionaryViewModel target, MapperContext context) - { - target.Key = source.Key; - target.Name = source.ItemKey; - target.ParentId = source.ParentId ?? null; - } - - // Umbraco.Code.MapAll -Level -Translations - private void Map(IDictionaryItem source, DictionaryOverviewViewModel target, MapperContext context) - { - target.Key = source.Key; - target.Name = source.ItemKey; - - // add all languages and the translations - foreach (ILanguage lang in _localizationService.GetAllLanguages()) - { - var langId = lang.Id; - IDictionaryTranslation? translation = source.Translations?.FirstOrDefault(x => x.LanguageId == langId); - - target.Translations.Add( - new DictionaryTranslationOverviewViewModel - { - DisplayName = lang.CultureName, - HasTranslation = translation != null && string.IsNullOrEmpty(translation.Value) == false, - }); - } - } -} diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 3b689525c8..166849c65e 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -556,18 +556,21 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DictionaryItem" + "$ref": "#/components/schemas/DictionaryItemCreateModel" } } } }, "responses": { "201": { - "description": "Created", + "description": "Created" + }, + "404": { + "description": "Not Found", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreatedResult" + "$ref": "#/components/schemas/ProblemDetails" } } } @@ -581,56 +584,13 @@ } } } - } - } - } - }, - "/umbraco/management/api/v1/dictionary/{id}": { - "patch": { - "tags": [ - "Dictionary" - ], - "operationId": "PatchDictionaryById", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/JsonPatch" - } - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ContentResult" - } - } - } }, - "404": { - "description": "Not Found", + "409": { + "description": "Conflict", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundResult" + "$ref": "#/components/schemas/ProblemDetails" } } } @@ -661,7 +621,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Dictionary" + "$ref": "#/components/schemas/DictionaryItem" } } } @@ -671,7 +631,58 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundResult" + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + }, + "put": { + "tags": [ + "Dictionary" + ], + "operationId": "PutDictionaryByKey", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DictionaryItemUpdateModel" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" } } } @@ -703,7 +714,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundResult" + "$ref": "#/components/schemas/ProblemDetails" } } } @@ -815,7 +826,7 @@ ], "operationId": "PostDictionaryUpload", "requestBody": { - "content": {} + "content": { } }, "responses": { "200": { @@ -1216,16 +1227,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PagedRecycleBinItem" - } - } - } - }, "401": { "description": "Unauthorized", "content": { @@ -1235,6 +1236,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedRecycleBinItem" + } + } + } } } } @@ -1266,16 +1277,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PagedRecycleBinItem" - } - } - } - }, "401": { "description": "Unauthorized", "content": { @@ -1285,6 +1286,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedRecycleBinItem" + } + } + } } } } @@ -1515,16 +1526,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HealthCheckGroupWithResult" - } - } - } - }, "404": { "description": "Not Found", "content": { @@ -1534,6 +1535,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthCheckGroupWithResult" + } + } + } } } } @@ -1554,16 +1565,6 @@ } }, "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HealthCheckResult" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -1573,6 +1574,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthCheckResult" + } + } + } } } } @@ -1624,16 +1635,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PagedHelpPage" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -1643,6 +1644,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedHelpPage" + } + } + } } } } @@ -1702,16 +1713,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Index" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -1721,6 +1722,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Index" + } + } + } } } } @@ -1742,16 +1753,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OkResult" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -1761,6 +1762,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OkResult" + } + } + } } } } @@ -1772,16 +1783,6 @@ ], "operationId": "GetInstallSettings", "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstallSettings" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -1801,6 +1802,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstallSettings" + } + } + } } } } @@ -1821,9 +1832,6 @@ } }, "responses": { - "200": { - "description": "Success" - }, "400": { "description": "Bad Request", "content": { @@ -1843,6 +1851,9 @@ } } } + }, + "200": { + "description": "Success" } } } @@ -1863,9 +1874,6 @@ } }, "responses": { - "200": { - "description": "Success" - }, "400": { "description": "Bad Request", "content": { @@ -1875,6 +1883,9 @@ } } } + }, + "200": { + "description": "Success" } } } @@ -1895,9 +1906,6 @@ } }, "responses": { - "201": { - "description": "Created" - }, "400": { "description": "Bad Request", "content": { @@ -1907,6 +1915,9 @@ } } } + }, + "201": { + "description": "Created" } } }, @@ -1965,16 +1976,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Language" - } - } - } - }, "404": { "description": "Not Found", "content": { @@ -1984,6 +1985,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Language" + } + } + } } } }, @@ -2004,9 +2015,6 @@ } ], "responses": { - "200": { - "description": "Success" - }, "400": { "description": "Bad Request", "content": { @@ -2026,6 +2034,9 @@ } } } + }, + "200": { + "description": "Success" } } }, @@ -2055,8 +2066,15 @@ } }, "responses": { - "200": { - "description": "Success" + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundResult" + } + } + } }, "400": { "description": "Bad Request", @@ -2068,15 +2086,8 @@ } } }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundResult" - } - } - } + "200": { + "description": "Success" } } } @@ -2256,16 +2267,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PagedRecycleBinItem" - } - } - } - }, "401": { "description": "Unauthorized", "content": { @@ -2275,6 +2276,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedRecycleBinItem" + } + } + } } } } @@ -2306,16 +2317,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PagedRecycleBinItem" - } - } - } - }, "401": { "description": "Unauthorized", "content": { @@ -2325,6 +2326,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedRecycleBinItem" + } + } + } } } } @@ -2932,16 +2943,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PagedRedirectUrl" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -2951,6 +2952,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedRedirectUrl" + } + } + } } } } @@ -3490,16 +3501,6 @@ ], "operationId": "GetServerStatus", "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServerStatus" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -3509,6 +3510,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServerStatus" + } + } + } } } } @@ -3520,16 +3531,6 @@ ], "operationId": "GetServerVersion", "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Version" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -3539,6 +3540,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Version" + } + } + } } } } @@ -3859,9 +3870,6 @@ } }, "responses": { - "200": { - "description": "Success" - }, "400": { "description": "Bad Request", "content": { @@ -3871,6 +3879,9 @@ } } } + }, + "200": { + "description": "Success" } } } @@ -4556,23 +4567,6 @@ }, "additionalProperties": false }, - "BackOfficeNotification": { - "type": "object", - "properties": { - "header": { - "type": "string", - "nullable": true - }, - "message": { - "type": "string", - "nullable": true - }, - "notificationType": { - "$ref": "#/components/schemas/NotificationStyle" - } - }, - "additionalProperties": false - }, "CallingConventions": { "enum": [ "Standard", @@ -4726,63 +4720,6 @@ }, "additionalProperties": false }, - "ContentApp": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true - }, - "alias": { - "type": "string", - "nullable": true - }, - "weight": { - "type": "integer", - "format": "int32" - }, - "icon": { - "type": "string", - "nullable": true - }, - "view": { - "type": "string", - "nullable": true - }, - "viewModel": { - "nullable": true - }, - "active": { - "type": "boolean" - }, - "badge": { - "$ref": "#/components/schemas/ContentAppBadge" - } - }, - "additionalProperties": false - }, - "ContentAppBadge": { - "type": "object", - "properties": { - "count": { - "type": "integer", - "format": "int32" - }, - "type": { - "$ref": "#/components/schemas/ContentAppBadgeType" - } - }, - "additionalProperties": false - }, - "ContentAppBadgeType": { - "enum": [ - "default", - "warning", - "alert" - ], - "type": "integer", - "format": "int32" - }, "ContentResult": { "type": "object", "properties": { @@ -5145,50 +5082,6 @@ }, "additionalProperties": false }, - "Dictionary": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "parentId": { - "type": "string", - "format": "uuid", - "nullable": true - }, - "translations": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DictionaryTranslation" - } - }, - "contentApps": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ContentApp" - } - }, - "notifications": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BackOfficeNotification" - }, - "readOnly": true - }, - "name": { - "minLength": 1, - "type": "string" - }, - "key": { - "type": "string", - "format": "uuid" - }, - "path": { - "type": "string" - } - }, - "additionalProperties": false - }, "DictionaryImport": { "type": "object", "properties": { @@ -5208,10 +5101,14 @@ "DictionaryItem": { "type": "object", "properties": { - "parentId": { - "type": "string", - "format": "uuid", - "nullable": true + "name": { + "type": "string" + }, + "translations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DictionaryItemTranslationModel" + } }, "key": { "type": "string", @@ -5220,6 +5117,53 @@ }, "additionalProperties": false }, + "DictionaryItemCreateModel": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "translations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DictionaryItemTranslationModel" + } + }, + "parentKey": { + "type": "string", + "format": "uuid", + "nullable": true + } + }, + "additionalProperties": false + }, + "DictionaryItemTranslationModel": { + "type": "object", + "properties": { + "isoCode": { + "type": "string" + }, + "translation": { + "type": "string" + } + }, + "additionalProperties": false + }, + "DictionaryItemUpdateModel": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "translations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DictionaryItemTranslationModel" + } + } + }, + "additionalProperties": false + }, "DictionaryItemsImport": { "type": "object", "properties": { @@ -5259,35 +5203,6 @@ }, "additionalProperties": false }, - "DictionaryTranslation": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32" - }, - "key": { - "type": "string", - "format": "uuid" - }, - "displayName": { - "type": "string", - "nullable": true - }, - "isoCode": { - "type": "string", - "nullable": true - }, - "translation": { - "type": "string" - }, - "languageId": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, "DictionaryTranslationOverview": { "type": "object", "properties": { @@ -5987,7 +5902,7 @@ }, "providerProperties": { "type": "object", - "additionalProperties": {}, + "additionalProperties": { }, "nullable": true } }, @@ -6031,19 +5946,6 @@ "type": "object", "additionalProperties": false }, - "JsonPatch": { - "type": "object", - "properties": { - "op": { - "type": "string" - }, - "path": { - "type": "string" - }, - "value": { } - }, - "additionalProperties": false - }, "Language": { "required": [ "isoCode" @@ -6567,17 +6469,6 @@ }, "additionalProperties": false }, - "NotificationStyle": { - "enum": [ - "Save", - "Info", - "Error", - "Success", - "Warning" - ], - "type": "integer", - "format": "int32" - }, "OkResult": { "type": "object", "properties": { @@ -7131,7 +7022,7 @@ "nullable": true } }, - "additionalProperties": {} + "additionalProperties": { } }, "ProfilingStatus": { "type": "object", @@ -8477,7 +8368,7 @@ "authorizationCode": { "authorizationUrl": "/umbraco/management/api/v1.0/security/back-office/authorize", "tokenUrl": "/umbraco/management/api/v1.0/security/back-office/token", - "scopes": {} + "scopes": { } } } } @@ -8485,7 +8376,7 @@ }, "security": [ { - "OAuth": [] + "OAuth": [ ] } ] } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemCreateModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemCreateModel.cs new file mode 100644 index 0000000000..b2cbe45f3c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemCreateModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Dictionary; + +public class DictionaryItemCreateModel : DictionaryItemModelBase +{ + public Guid? ParentKey { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemModelBase.cs new file mode 100644 index 0000000000..ebdf72fa30 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemModelBase.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Dictionary; + +public class DictionaryItemModelBase +{ + public string Name { get; set; } = null!; + + public IEnumerable Translations { get; set; } = Array.Empty(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemTranslationModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemTranslationModel.cs new file mode 100644 index 0000000000..57d08380b2 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemTranslationModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Dictionary; + +public class DictionaryItemTranslationModel +{ + public string IsoCode { get; set; } = string.Empty; + + public string Translation { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemUpdateModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemUpdateModel.cs new file mode 100644 index 0000000000..07835c2c91 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemUpdateModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Dictionary; + +public class DictionaryItemUpdateModel : DictionaryItemModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemViewModel.cs index 5616705c05..a7ee7e5b32 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemViewModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemViewModel.cs @@ -1,8 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Dictionary; -public class DictionaryItemViewModel +public class DictionaryItemViewModel : DictionaryItemModelBase { - public Guid? ParentId { get; set; } - public Guid Key { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryTranslationViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryTranslationViewModel.cs deleted file mode 100644 index 9d82f93f1e..0000000000 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryTranslationViewModel.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Umbraco.Cms.Api.Management.ViewModels.Dictionary; - -public class DictionaryTranslationViewModel -{ - public int Id { get; set; } - - public Guid Key { get; set; } - - /// - /// Gets or sets the display name. - /// - public string? DisplayName { get; set; } - - /// - /// Gets or sets the ISO code. - /// - public string? IsoCode { get; set; } - - /// - /// Gets or sets the translation. - /// - public string Translation { get; set; } = null!; - - /// - /// Gets or sets the language id. - /// - public int LanguageId { get; set; } -} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryViewModel.cs deleted file mode 100644 index 03fc307862..0000000000 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryViewModel.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Umbraco.Cms.Core.Models.ContentEditing; -using Umbraco.Cms.Core.Models.Validation; - -namespace Umbraco.Cms.Api.Management.ViewModels.Dictionary; - -/// -/// The dictionary display model -/// -public class DictionaryViewModel : INotificationModel -{ - /// - /// Initializes a new instance of the class. - /// - public DictionaryViewModel() - { - Notifications = new List(); - Translations = new List(); - ContentApps = new List(); - } - - /// - /// Gets or sets the parent id. - /// - public Guid? ParentId { get; set; } - - /// - /// Gets or sets the translations. - /// - public IEnumerable Translations { get; set; } = Enumerable.Empty(); - - /// - /// Apps for the dictionary item - /// - public IEnumerable ContentApps { get; set; } - - /// - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - public List Notifications { get; private set; } - - [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] - [Required] - public string Name { get; set; } = null!; - - /// - /// Gets or sets the Key for the object - /// - public Guid Key { get; set; } - - /// - /// The path of the entity - /// - public string Path { get; set; } = string.Empty; -} diff --git a/src/Umbraco.Core/CompatibilitySuppressions.xml b/src/Umbraco.Core/CompatibilitySuppressions.xml index e6196e4796..77f7c39d35 100644 --- a/src/Umbraco.Core/CompatibilitySuppressions.xml +++ b/src/Umbraco.Core/CompatibilitySuppressions.xml @@ -301,6 +301,27 @@ lib/net7.0/Umbraco.Core.dll true + + CP0006 + M:Umbraco.Cms.Core.Services.ILocalizationService.Create(System.String,System.Nullable{System.Guid},System.Collections.Generic.IEnumerable{Umbraco.Cms.Core.Models.IDictionaryTranslation},System.Int32) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0006 + M:Umbraco.Cms.Core.Services.ILocalizationService.Delete(System.Guid,System.Int32) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0006 + M:Umbraco.Cms.Core.Services.ILocalizationService.Update(Umbraco.Cms.Core.Models.IDictionaryItem,System.Int32) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + CP0006 P:Umbraco.Cms.Core.Models.IDataType.ConfigurationData diff --git a/src/Umbraco.Core/Services/ILocalizationService.cs b/src/Umbraco.Core/Services/ILocalizationService.cs index dbfb01d3e1..7cd403b4ab 100644 --- a/src/Umbraco.Core/Services/ILocalizationService.cs +++ b/src/Umbraco.Core/Services/ILocalizationService.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Core.Services; @@ -30,8 +31,23 @@ public interface ILocalizationService : IService /// /// /// + [Obsolete("Please use Create. Will be removed in V15")] IDictionaryItem CreateDictionaryItemWithIdentity(string key, Guid? parentId, string? defaultValue = null); + /// + /// Creates and saves a new dictionary item and assigns translations to all applicable languages if specified + /// + /// + /// + /// + /// Optional id of the user saving the dictionary item + /// + Attempt Create( + string key, + Guid? parentId, + IEnumerable? translations = null, + int userId = Constants.Security.SuperUserId); + /// /// Gets a by its id /// @@ -109,16 +125,33 @@ public interface ILocalizationService : IService /// /// to save /// Optional id of the user saving the dictionary item + [Obsolete("Please use Update. Will be removed in V15")] void Save(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId); + /// + /// Updates an existing object + /// + /// to update + /// Optional id of the user saving the dictionary item + Attempt Update(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId); + /// /// Deletes a object and its related translations /// as well as its children. /// /// to delete /// Optional id of the user deleting the dictionary item + [Obsolete("Please use the Delete method that takes an ID and returns an Attempt. Will be removed in V15")] void Delete(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId); + /// + /// Deletes a object and its related translations + /// as well as its children. + /// + /// The ID of the to delete + /// Optional id of the user deleting the dictionary item + Attempt Delete(Guid id, int userId = Constants.Security.SuperUserId); + /// /// Gets a by its id /// diff --git a/src/Umbraco.Core/Services/LocalizationService.cs b/src/Umbraco.Core/Services/LocalizationService.cs index 824aed8cf5..73faa6c460 100644 --- a/src/Umbraco.Core/Services/LocalizationService.cs +++ b/src/Umbraco.Core/Services/LocalizationService.cs @@ -2,9 +2,9 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services; @@ -83,7 +83,27 @@ internal class LocalizationService : RepositoryService, ILocalizationService /// /// /// + [Obsolete("Please use Create. Will be removed in V15")] public IDictionaryItem CreateDictionaryItemWithIdentity(string key, Guid? parentId, string? defaultValue = null) + { + IEnumerable translations = defaultValue.IsNullOrWhiteSpace() + ? Array.Empty() + : GetAllLanguages() + .Select(language => new DictionaryTranslation(language, defaultValue!)) + .ToArray(); + + Attempt result = Create(key, parentId, translations); + return result.Success + ? result.Result! + : throw new ArgumentException($"Could not create a dictionary item with key: {key} under parent: {parentId}"); + } + + /// + public Attempt Create( + string key, + Guid? parentId, + IEnumerable? translations = null, + int userId = Constants.Security.SuperUserId) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { @@ -93,20 +113,23 @@ internal class LocalizationService : RepositoryService, ILocalizationService IDictionaryItem? parent = GetDictionaryItemById(parentId.Value); if (parent == null) { - throw new ArgumentException($"No parent dictionary item was found with id {parentId.Value}."); + return Attempt.FailWithStatus(DictionaryItemOperationStatus.ParentNotFound, null); } } var item = new DictionaryItem(parentId, key); - if (defaultValue.IsNullOrWhiteSpace() == false) + // do we have an item key collision (item keys must be unique)? + if (HasItemKeyCollision(item)) { - IEnumerable langs = GetAllLanguages(); - var translations = langs.Select(language => new DictionaryTranslation(language, defaultValue!)) - .Cast() - .ToList(); + return Attempt.FailWithStatus(DictionaryItemOperationStatus.DuplicateItemKey, null); + } - item.Translations = translations; + IDictionaryTranslation[] translationsAsArray = translations?.ToArray() ?? Array.Empty(); + if (translationsAsArray.Any()) + { + var allLanguageIds = GetAllLanguages().Select(language => language.Id).ToArray(); + item.Translations = translationsAsArray.Where(translation => allLanguageIds.Contains(translation.LanguageId)).ToArray(); } EventMessages eventMessages = EventMessagesFactory.Get(); @@ -115,7 +138,7 @@ internal class LocalizationService : RepositoryService, ILocalizationService if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); - return item; + return Attempt.FailWithStatus(DictionaryItemOperationStatus.CancelledByNotification, item); } _dictionaryRepository.Save(item); @@ -126,9 +149,10 @@ internal class LocalizationService : RepositoryService, ILocalizationService scope.Notifications.Publish( new DictionaryItemSavedNotification(item, eventMessages).WithStateFrom(savingNotification)); + Audit(AuditType.New, "Create DictionaryItem", userId, item.Id, "DictionaryItem"); scope.Complete(); - return item; + return Attempt.SucceedWithStatus(DictionaryItemOperationStatus.Success, item); } } @@ -310,6 +334,7 @@ internal class LocalizationService : RepositoryService, ILocalizationService /// /// to save /// Optional id of the user saving the dictionary item + [Obsolete("Please use Update. Will be removed in V15")] public void Save(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -335,22 +360,72 @@ internal class LocalizationService : RepositoryService, ILocalizationService } } + /// + public Attempt Update(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + // is there an item to update? + if (_dictionaryRepository.Exists(dictionaryItem.Id) == false) + { + return Attempt.FailWithStatus(DictionaryItemOperationStatus.ItemNotFound, dictionaryItem); + } + + // do we have an item key collision (item keys must be unique)? + if (HasItemKeyCollision(dictionaryItem)) + { + return Attempt.FailWithStatus(DictionaryItemOperationStatus.DuplicateItemKey, dictionaryItem); + } + + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new DictionaryItemSavingNotification(dictionaryItem, eventMessages); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + scope.Complete(); + return Attempt.FailWithStatus(DictionaryItemOperationStatus.CancelledByNotification, dictionaryItem); + } + + _dictionaryRepository.Save(dictionaryItem); + + // ensure the lazy Language callback is assigned + EnsureDictionaryItemLanguageCallback(dictionaryItem); + scope.Notifications.Publish( + new DictionaryItemSavedNotification(dictionaryItem, eventMessages).WithStateFrom(savingNotification)); + + Audit(AuditType.Save, "Update DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem"); + scope.Complete(); + + return Attempt.SucceedWithStatus(DictionaryItemOperationStatus.Success, dictionaryItem); + } + } + /// /// Deletes a object and its related translations /// as well as its children. /// /// to delete /// Optional id of the user deleting the dictionary item + [Obsolete("Please use the Delete method that takes an ID and returns an Attempt. Will be removed in V15")] public void Delete(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId) + => Delete(dictionaryItem.Key, userId); + + /// + public Attempt Delete(Guid id, int userId = Constants.Security.SuperUserId) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { + IDictionaryItem? dictionaryItem = _dictionaryRepository.Get(id); + if (dictionaryItem == null) + { + return Attempt.FailWithStatus(DictionaryItemOperationStatus.ItemNotFound, null); + } + EventMessages eventMessages = EventMessagesFactory.Get(); var deletingNotification = new DictionaryItemDeletingNotification(dictionaryItem, eventMessages); if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); - return; + return Attempt.FailWithStatus(DictionaryItemOperationStatus.CancelledByNotification, dictionaryItem); } _dictionaryRepository.Delete(dictionaryItem); @@ -361,6 +436,7 @@ internal class LocalizationService : RepositoryService, ILocalizationService Audit(AuditType.Delete, "Delete DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem"); scope.Complete(); + return Attempt.SucceedWithStatus(DictionaryItemOperationStatus.Success, dictionaryItem); } } @@ -589,4 +665,10 @@ internal class LocalizationService : RepositoryService, ILocalizationService } } } + + private bool HasItemKeyCollision(IDictionaryItem dictionaryItem) + { + IDictionaryItem? itemKeyCollision = _dictionaryRepository.Get(dictionaryItem.ItemKey); + return itemKeyCollision != null && itemKeyCollision.Key != dictionaryItem.Key; + } } diff --git a/src/Umbraco.Core/Services/OperationStatus/DictionaryItemOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/DictionaryItemOperationStatus.cs new file mode 100644 index 0000000000..d15e97c7ba --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/DictionaryItemOperationStatus.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum DictionaryItemOperationStatus +{ + Success, + CancelledByNotification, + DuplicateItemKey, + ItemNotFound, + ParentNotFound +} diff --git a/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml b/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml new file mode 100644 index 0000000000..e8bace47de --- /dev/null +++ b/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml @@ -0,0 +1,10 @@ + + + + CP0002 + M:Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.LocalizationServiceTests.Can_Create_DictionaryItem_At_Root_With_Identity + lib/net7.0/Umbraco.Tests.Integration.dll + lib/net7.0/Umbraco.Tests.Integration.dll + true + + \ No newline at end of file diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Packaging/CreatedPackagesRepositoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Packaging/CreatedPackagesRepositoryTests.cs index 08dddf715d..f526541bbb 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Packaging/CreatedPackagesRepositoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Packaging/CreatedPackagesRepositoryTests.cs @@ -168,16 +168,11 @@ public class CreatedPackagesRepositoryTests : UmbracoIntegrationTest [Test] public void GivenNestedDictionaryItems_WhenPackageExported_ThenTheXmlIsNested() { - var parent = new DictionaryItem("Parent") { Key = Guid.NewGuid() }; - LocalizationService.Save(parent); - var child1 = new DictionaryItem(parent.Key, "Child1") { Key = Guid.NewGuid() }; - LocalizationService.Save(child1); - var child2 = new DictionaryItem(child1.Key, "Child2") { Key = Guid.NewGuid() }; - LocalizationService.Save(child2); - var child3 = new DictionaryItem(child2.Key, "Child3") { Key = Guid.NewGuid() }; - LocalizationService.Save(child3); - var child4 = new DictionaryItem(child3.Key, "Child4") { Key = Guid.NewGuid() }; - LocalizationService.Save(child4); + var parent = LocalizationService.Create("Parent", null).Result!; + var child1 = LocalizationService.Create("Child1", parent.Key).Result!; + var child2 = LocalizationService.Create("Child2", child1.Key).Result!; + var child3 = LocalizationService.Create("Child3", child2.Key).Result!; + var child4 = LocalizationService.Create("Child4", child3.Key).Result!; var def = new PackageDefinition { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs index 628fa419c2..de1c0b33f0 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs @@ -404,20 +404,14 @@ public class DictionaryRepositoryTest : UmbracoIntegrationTest var languageDK = new Language("da-DK", "Danish (Denmark)"); localizationService.Save(languageDK); //Id 2 - var readMore = new DictionaryItem("Read More"); - var translations = new List + localizationService.Create("Read More", null, new List { new DictionaryTranslation(language, "Read More"), new DictionaryTranslation(languageDK, "Læs mere") - }; - readMore.Translations = translations; - localizationService.Save(readMore); // Id 1 + }); // Id 1 - var article = new DictionaryItem("Article"); - var translations2 = new List + localizationService.Create("Article", null, new List { new DictionaryTranslation(language, "Article"), new DictionaryTranslation(languageDK, "Artikel") - }; - article.Translations = translations2; - localizationService.Save(article); // Id 2 + }); // Id 2 } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs index dfc5ee8bb7..b0d3dfcd1a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs @@ -240,9 +240,10 @@ public class ScopedRepositoryTests : UmbracoIntegrationTest var lang = new Language("fr-FR", "French (France)"); service.Save(lang); - var item = (IDictionaryItem)new DictionaryItem("item-key"); - item.Translations = new IDictionaryTranslation[] { new DictionaryTranslation(lang.Id, "item-value") }; - service.Save(item); + var item = service.Create( + "item-key", + null, + new IDictionaryTranslation[] { new DictionaryTranslation(lang.Id, "item-value") }).Result!; // Refresh the cache manually because we can't unbind service.GetDictionaryItemById(item.Id); @@ -266,7 +267,7 @@ public class ScopedRepositoryTests : UmbracoIntegrationTest Assert.AreNotSame(globalCache, scopedCache); item.ItemKey = "item-changed"; - service.Save(item); + service.Update(item); // scoped cache contains the "new" entity var scopeCached = (IDictionaryItem)scopedCache.Get(GetCacheIdKey(item.Id), () => null); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/LocalizationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/LocalizationServiceTests.cs index 2ca4af5fae..b93e7ea77d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/LocalizationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/LocalizationServiceTests.cs @@ -1,13 +1,11 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using NUnit.Framework; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; @@ -136,26 +134,27 @@ public class LocalizationServiceTests : UmbracoIntegrationTest for (var i = 0; i < 25; i++) { // Create 2 per level - var desc1 = new DictionaryItem(currParentId, "D1" + i) - { - Translations = new List + var result = LocalizationService.Create( + "D1" + i, + currParentId, + new List { new DictionaryTranslation(en, "ChildValue1 " + i), new DictionaryTranslation(dk, "BørnVærdi1 " + i) - } - }; - var desc2 = new DictionaryItem(currParentId, "D2" + i) - { - Translations = new List + }); + + Assert.IsTrue(result.Success); + + LocalizationService.Create( + "D2" + i, + currParentId, + new List { new DictionaryTranslation(en, "ChildValue2 " + i), new DictionaryTranslation(dk, "BørnVærdi2 " + i) - } - }; - LocalizationService.Save(desc1); - LocalizationService.Save(desc2); + }); - currParentId = desc1.Key; + currParentId = result.Result!.Key; } ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = true; @@ -242,14 +241,12 @@ public class LocalizationServiceTests : UmbracoIntegrationTest { var english = LocalizationService.GetLanguageByIsoCode("en-US"); - var item = (IDictionaryItem)new DictionaryItem("Testing123") - { - Translations = new List { new DictionaryTranslation(english, "Hello world") } - }; - LocalizationService.Save(item); + var result = LocalizationService.Create("Testing123", null, new List { new DictionaryTranslation(english, "Hello world") }); + Assert.True(result.Success); // re-get - item = LocalizationService.GetDictionaryItemById(item.Id); + var item = LocalizationService.GetDictionaryItemById(result.Result!.Id); + Assert.NotNull(item); Assert.Greater(item.Id, 0); Assert.IsTrue(item.HasIdentity); @@ -259,42 +256,75 @@ public class LocalizationServiceTests : UmbracoIntegrationTest } [Test] - public void Can_Create_DictionaryItem_At_Root_With_Identity() + public void Can_Create_DictionaryItem_At_Root_With_All_Languages() { - var item = LocalizationService.CreateDictionaryItemWithIdentity( - "Testing12345", null, "Hellooooo"); + var allLangs = LocalizationService.GetAllLanguages().ToArray(); + Assert.Greater(allLangs.Length, 0); + + var translations = allLangs.Select(language => new DictionaryTranslation(language, $"Translation for: {language.IsoCode}")).ToArray(); + var result = LocalizationService.Create("Testing12345", null, translations); + + Assert.IsTrue(result.Success); + Assert.AreEqual(DictionaryItemOperationStatus.Success, result.Status); + Assert.NotNull(result.Result); // re-get - item = LocalizationService.GetDictionaryItemById(item.Id); + var item = LocalizationService.GetDictionaryItemById(result.Result!.Id); Assert.IsNotNull(item); Assert.Greater(item.Id, 0); Assert.IsTrue(item.HasIdentity); Assert.IsFalse(item.ParentId.HasValue); Assert.AreEqual("Testing12345", item.ItemKey); - var allLangs = LocalizationService.GetAllLanguages(); - Assert.Greater(allLangs.Count(), 0); foreach (var language in allLangs) { - Assert.AreEqual("Hellooooo", + Assert.AreEqual($"Translation for: {language.IsoCode}", item.Translations.Single(x => x.Language.CultureName == language.CultureName).Value); } } + [Test] + public void Can_Create_DictionaryItem_At_Root_With_Some_Languages() + { + var allLangs = LocalizationService.GetAllLanguages().ToArray(); + Assert.Greater(allLangs.Length, 1); + + var firstLanguage = allLangs.First(); + var translations = new[] { new DictionaryTranslation(firstLanguage, $"Translation for: {firstLanguage.IsoCode}") }; + var result = LocalizationService.Create("Testing12345", null, translations); + + Assert.IsTrue(result.Success); + Assert.AreEqual(DictionaryItemOperationStatus.Success, result.Status); + Assert.NotNull(result.Result); + + // re-get + var item = LocalizationService.GetDictionaryItemById(result.Result!.Id); + + Assert.IsNotNull(item); + Assert.Greater(item.Id, 0); + Assert.IsTrue(item.HasIdentity); + Assert.IsFalse(item.ParentId.HasValue); + Assert.AreEqual("Testing12345", item.ItemKey); + Assert.AreEqual(1, item.Translations.Count()); + Assert.AreEqual(firstLanguage.Id, item.Translations.First().LanguageId); + } + [Test] public void Can_Add_Translation_To_Existing_Dictionary_Item() { var english = LocalizationService.GetLanguageByIsoCode("en-US"); - var item = (IDictionaryItem)new DictionaryItem("Testing123"); - LocalizationService.Save(item); + var result = LocalizationService.Create("Testing123", null); + Assert.True(result.Success); // re-get - item = LocalizationService.GetDictionaryItemById(item.Id); + var item = LocalizationService.GetDictionaryItemById(result.Result!.Id); + Assert.NotNull(item); item.Translations = new List { new DictionaryTranslation(english, "Hello world") }; - LocalizationService.Save(item); + result = LocalizationService.Update(item); + Assert.True(result.Success); Assert.AreEqual(1, item.Translations.Count()); foreach (var translation in item.Translations) @@ -309,10 +339,12 @@ public class LocalizationServiceTests : UmbracoIntegrationTest "My new value") }; - LocalizationService.Save(item); + result = LocalizationService.Update(item); + Assert.True(result.Success); // re-get item = LocalizationService.GetDictionaryItemById(item.Id); + Assert.NotNull(item); Assert.AreEqual(2, item.Translations.Count()); Assert.AreEqual("Hello world", item.Translations.First().Value); @@ -325,7 +357,9 @@ public class LocalizationServiceTests : UmbracoIntegrationTest var item = LocalizationService.GetDictionaryItemByKey("Child"); Assert.NotNull(item); - LocalizationService.Delete(item); + var result = LocalizationService.Delete(item.Key); + Assert.IsTrue(result.Success); + Assert.AreEqual(DictionaryItemOperationStatus.Success, result.Status); var deletedItem = LocalizationService.GetDictionaryItemByKey("Child"); Assert.Null(deletedItem); @@ -340,7 +374,8 @@ public class LocalizationServiceTests : UmbracoIntegrationTest translation.Value += "UPDATED"; } - LocalizationService.Save(item); + var result = LocalizationService.Update(item); + Assert.True(result.Success); var updatedItem = LocalizationService.GetDictionaryItemByKey("Child"); Assert.NotNull(updatedItem); @@ -437,6 +472,78 @@ public class LocalizationServiceTests : UmbracoIntegrationTest Assert.Null(result); } + [Test] + public void Cannot_Add_Duplicate_DictionaryItem_Key() + { + var item = LocalizationService.GetDictionaryItemByKey("Child"); + Assert.IsNotNull(item); + + item.ItemKey = "Parent"; + + var result = LocalizationService.Update(item); + Assert.IsFalse(result.Success); + Assert.AreEqual(DictionaryItemOperationStatus.DuplicateItemKey, result.Status); + + var item2 = LocalizationService.GetDictionaryItemByKey("Child"); + Assert.IsNotNull(item2); + Assert.AreEqual(item.Key, item2.Key); + } + + [Test] + public void Cannot_Create_Child_DictionaryItem_Under_Missing_Parent() + { + var itemKey = Guid.NewGuid().ToString("N"); + + var result = LocalizationService.Create(itemKey, Guid.NewGuid(), Array.Empty()); + Assert.IsFalse(result.Success); + Assert.AreEqual(DictionaryItemOperationStatus.ParentNotFound, result.Status); + + var item = LocalizationService.GetDictionaryItemByKey(itemKey); + Assert.IsNull(item); + } + + [Test] + public void Cannot_Create_Multiple_DictionaryItems_With_Same_ItemKey() + { + var itemKey = Guid.NewGuid().ToString("N"); + var result = LocalizationService.Create(itemKey, null, Array.Empty()); + + Assert.IsTrue(result.Success); + + result = LocalizationService.Create(itemKey, null, Array.Empty()); + Assert.IsFalse(result.Success); + Assert.AreEqual(DictionaryItemOperationStatus.DuplicateItemKey, result.Status); + } + + [Test] + public void Cannot_Update_Non_Existant_DictionaryItem() + { + var result = LocalizationService.Update(new DictionaryItem("NoSuchItemKey")); + Assert.False(result.Success); + Assert.AreEqual(DictionaryItemOperationStatus.ItemNotFound, result.Status); + } + + [Test] + public void Cannot_Update_DictionaryItem_With_Empty_Id() + { + var item = LocalizationService.GetDictionaryItemByKey("Child"); + Assert.IsNotNull(item); + + item = new DictionaryItem(item.ParentId, item.ItemKey) { Key = item.Key, Translations = item.Translations }; + + var result = LocalizationService.Update(item); + Assert.False(result.Success); + Assert.AreEqual(DictionaryItemOperationStatus.ItemNotFound, result.Status); + } + + [Test] + public void Cannot_Delete_Non_Existant_DictionaryItem() + { + var result = LocalizationService.Delete(Guid.NewGuid()); + Assert.IsFalse(result.Success); + Assert.AreEqual(DictionaryItemOperationStatus.ItemNotFound, result.Status); + } + public void CreateTestData() { var languageDaDk = new LanguageBuilder() @@ -451,27 +558,31 @@ public class LocalizationServiceTests : UmbracoIntegrationTest _danishLangId = languageDaDk.Id; _englishLangId = languageEnGb.Id; - var parentItem = new DictionaryItem("Parent") - { - Translations = new List + var result = LocalizationService.Create( + "Parent", + null, + new List { new DictionaryTranslation(languageEnGb, "ParentValue"), new DictionaryTranslation(languageDaDk, "ForældreVærdi") - } - }; - LocalizationService.Save(parentItem); + }); + Assert.True(result.Success); + IDictionaryItem parentItem = result.Result!; + _parentItemGuidId = parentItem.Key; _parentItemIntId = parentItem.Id; - var childItem = new DictionaryItem(parentItem.Key, "Child") - { - Translations = new List + result = LocalizationService.Create( + "Child", + parentItem.Key, + new List { new DictionaryTranslation(languageEnGb, "ChildValue"), new DictionaryTranslation(languageDaDk, "BørnVærdi") - } - }; - LocalizationService.Save(childItem); + }); + Assert.True(result.Success); + IDictionaryItem childItem = result.Result!; + _childItemGuidId = childItem.Key; _childItemIntId = childItem.Id; }