diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 3ec84ea1ff..bb2af47e40 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -61,7 +61,7 @@ This guide describes each step to make your first contribution: ## Further contribution guides - [Before you start](contributing-before-you-start.md) -- [Finding your first issue: Up for grabs](contributing-first-issue.md) +- [Finding your first issue](contributing-first-issue.md) - [Contributing to the new backoffice](https://docs.umbraco.com/umbraco-backoffice/) - [Unwanted changes](contributing-unwanted-changes.md) - [Other ways to contribute](contributing-other-ways-to-contribute.md) diff --git a/.github/contributing-first-issue.md b/.github/contributing-first-issue.md index 8027d69519..e4e865b287 100644 --- a/.github/contributing-first-issue.md +++ b/.github/contributing-first-issue.md @@ -1,6 +1,8 @@ -## Finding your first issue: Up for grabs +## Finding your first issue -Umbraco HQ will regularly mark newly created issues on the issue tracker with [the `community/up-for-grabs` tag][up for grabs issues]. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. We encourage anyone to pick them up and help out. +Umbraco HQ will regularly mark newly created issues on the issue tracker with [the `community/up-for-grabs` tag][up for grabs issues]. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. In adding the label we will endeavour to provide some guidelines on how to go about the implementation, such that it aligns with the project. We encourage anyone to pick them up and help out. + +You don't need to restrict yourselves to issues that are specifically marked as "up for grabs" though. If you are running into a bug you have reported or found on the [issue tracker][issue tracker], it's not necessary to wait for HQ response. Feel free to dive in and try to provide a fix, raising questions as you need if you have concerns about the modifications necessary to resolve the problem. If you do start working on something, make sure to leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has. @@ -11,15 +13,15 @@ Great question! The short version goes like this: 1. **Fork** Create a fork of [`Umbraco-CMS` on GitHub][Umbraco CMS repo] - + ![Fork the repository](img/forkrepository.png) - + 1. **Clone** When GitHub has created your fork, you can clone it in your favorite Git tool - - ![Clone the fork](img/clonefork.png) - + + ![Clone the fork](img/clonefork.png) + 1. **Switch to the correct branch** Switch to the `contrib` branch @@ -90,4 +92,5 @@ You can get in touch with [the core contributors team][core collabs] in multiple [draft prs]: https://github.blog/2019-02-14-introducing-draft-pull-requests/ "Github's blog post providing details on draft pull requests" [contrib forum]: https://our.umbraco.com/forum/contributing-to-umbraco-cms/ [Umbraco CMS repo]: https://github.com/umbraco/Umbraco-CMS -[up for grabs issues]: https://github.com/umbraco/Umbraco-CMS/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs \ No newline at end of file +[up for grabs issues]: https://github.com/umbraco/Umbraco-CMS/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs +[issue tracker]: https://github.com/umbraco/Umbraco-CMS/issues \ No newline at end of file diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs index f7b540f034..66c6df0cea 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs @@ -88,15 +88,4 @@ public abstract class ContentCollectionControllerBase - /// Populates the signs for the collection response models. - /// - protected async Task PopulateSigns(IEnumerable itemViewModels) - { - foreach (ISignProvider signProvider in _signProviders.Where(x => x.CanProvideSigns())) - { - await signProvider.PopulateSignsAsync(itemViewModels); - } - } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/ByKeyDocumentCollectionController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/ByKeyDocumentCollectionController.cs index 5ab89ba274..83ef32972c 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/ByKeyDocumentCollectionController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/ByKeyDocumentCollectionController.cs @@ -85,7 +85,6 @@ public class ByKeyDocumentCollectionController : DocumentCollectionControllerBas } List collectionResponseModels = await _documentCollectionPresentationFactory.CreateCollectionModelAsync(collectionAttempt.Result!); - await PopulateSigns(collectionResponseModels); return CollectionResult(collectionResponseModels, collectionAttempt.Result!.Items.Total); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs index f67f880e57..d65183c824 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs @@ -17,25 +17,14 @@ public class ItemDocumentItemController : DocumentItemControllerBase { private readonly IEntityService _entityService; private readonly IDocumentPresentationFactory _documentPresentationFactory; - private readonly SignProviderCollection _signProviders; [ActivatorUtilitiesConstructor] public ItemDocumentItemController( IEntityService entityService, - IDocumentPresentationFactory documentPresentationFactory, - SignProviderCollection signProvider) + IDocumentPresentationFactory documentPresentationFactory) { _entityService = entityService; _documentPresentationFactory = documentPresentationFactory; - _signProviders = signProvider; - } - - [Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18")] - public ItemDocumentItemController( - IEntityService entityService, - IDocumentPresentationFactory documentPresentationFactory) - : this(entityService, documentPresentationFactory, StaticServiceProvider.Instance.GetRequiredService()) - { } [HttpGet] @@ -55,15 +44,6 @@ public class ItemDocumentItemController : DocumentItemControllerBase .OfType(); IEnumerable responseModels = documents.Select(_documentPresentationFactory.CreateItemResponseModel); - await PopulateSigns(responseModels); return Ok(responseModels); } - - private async Task PopulateSigns(IEnumerable itemViewModels) - { - foreach (ISignProvider signProvider in _signProviders.Where(x => x.CanProvideSigns())) - { - await signProvider.PopulateSignsAsync(itemViewModels); - } - } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/ByKeyMediaCollectionController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/ByKeyMediaCollectionController.cs index 184cfe4de0..7029e9311d 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/ByKeyMediaCollectionController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/ByKeyMediaCollectionController.cs @@ -84,7 +84,6 @@ public class ByKeyMediaCollectionController : MediaCollectionControllerBase } List collectionResponseModels = await _mediaCollectionPresentationFactory.CreateCollectionModelAsync(collectionAttempt.Result!); - await PopulateSigns(collectionResponseModels); return CollectionResult(collectionResponseModels, collectionAttempt.Result!.Items.Total); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs index 8a5cc27ece..1c7c438f3e 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs @@ -73,9 +73,7 @@ public abstract class EntityTreeControllerBase : ManagementApiControllerB } IEntitySlim? entity = siblings.FirstOrDefault(); - Guid? parentKey = entity?.ParentId > 0 - ? EntityService.GetKey(entity.ParentId, ItemObjectType).Result - : Constants.System.RootKey; + Guid? parentKey = GetParentKey(entity); TItem[] treeItemViewModels = MapTreeItemViewModels(parentKey, siblings); @@ -86,6 +84,14 @@ public abstract class EntityTreeControllerBase : ManagementApiControllerB return Ok(result); } + /// + /// Gets the parent key for an entity, or root if null or no parent. + /// + protected virtual Guid? GetParentKey(IEntitySlim? entity) => + entity?.ParentId > 0 + ? EntityService.GetKey(entity.ParentId, ItemObjectType).Result + : Constants.System.RootKey; + protected virtual async Task>> GetAncestors(Guid descendantKey, bool includeSelf = true) { IEntitySlim[] ancestorEntities = await GetAncestorEntitiesAsync(descendantKey, includeSelf); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs index d54e537637..6dfb8738ed 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs @@ -45,6 +45,30 @@ public abstract class FolderTreeControllerBase : NamedEntityTreeControlle protected abstract UmbracoObjectTypes FolderObjectType { get; } + /// + protected override Guid? GetParentKey(IEntitySlim? entity) + { + if (entity is null || entity.ParentId <= 0) + { + return Constants.System.RootKey; + } + + Attempt getKeyAttempt = EntityService.GetKey(entity.ParentId, ItemObjectType); + if (getKeyAttempt.Success) + { + return getKeyAttempt.Result; + } + + // Parent could be a folder, so try that too. + getKeyAttempt = EntityService.GetKey(entity.ParentId, FolderObjectType); + if (getKeyAttempt.Success) + { + return getKeyAttempt.Result; + } + + return Constants.System.RootKey; + } + protected void RenderFoldersOnly(bool foldersOnly) => _foldersOnly = foldersOnly; protected override IEntitySlim[] GetPagedRootEntities(int skip, int take, out long totalItems) diff --git a/src/Umbraco.Cms.Api.Management/Factories/ContentCollectionPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/ContentCollectionPresentationFactory.cs index 0607df5108..f151d6337b 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/ContentCollectionPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/ContentCollectionPresentationFactory.cs @@ -1,4 +1,7 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; @@ -13,9 +16,24 @@ public abstract class ContentCollectionPresentationFactory _mapper = mapper; + [Obsolete("Please use the controller with all parameters, will be removed in Umbraco 18")] + protected ContentCollectionPresentationFactory(IUmbracoMapper mapper) + : this( + mapper, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + protected ContentCollectionPresentationFactory( + IUmbracoMapper mapper, + SignProviderCollection signProviderCollection) + { + _mapper = mapper; + _signProviderCollection = signProviderCollection; + } public async Task> CreateCollectionModelAsync(ListViewPagedModel contentCollection) { @@ -36,8 +54,19 @@ public abstract class ContentCollectionPresentationFactory contentCollection, List collectionResponseModels) => Task.CompletedTask; + + private async Task PopulateSigns(IEnumerable models) + { + foreach (ISignProvider signProvider in _signProviderCollection.Where(x => x.CanProvideSigns())) + { + await signProvider.PopulateSignsAsync(models); + } + } } diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs index 51596fb476..c3d16271ce 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs @@ -1,10 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Mapping.Content; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.Document.Item; using Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint.Item; using Umbraco.Cms.Api.Management.ViewModels.DocumentType; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentPublishing; @@ -23,7 +26,9 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory private readonly IPublicAccessService _publicAccessService; private readonly TimeProvider _timeProvider; private readonly IIdKeyMap _idKeyMap; + private readonly SignProviderCollection _signProviderCollection; + [Obsolete("Please use the controller with all parameters. Scheduled for removal in Umbraco 18")] public DocumentPresentationFactory( IUmbracoMapper umbracoMapper, IDocumentUrlFactory documentUrlFactory, @@ -31,6 +36,25 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory IPublicAccessService publicAccessService, TimeProvider timeProvider, IIdKeyMap idKeyMap) + : this( + umbracoMapper, + documentUrlFactory, + templateService, + publicAccessService, + timeProvider, + idKeyMap, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public DocumentPresentationFactory( + IUmbracoMapper umbracoMapper, + IDocumentUrlFactory documentUrlFactory, + ITemplateService templateService, + IPublicAccessService publicAccessService, + TimeProvider timeProvider, + IIdKeyMap idKeyMap, + SignProviderCollection signProviderCollection) { _umbracoMapper = umbracoMapper; _documentUrlFactory = documentUrlFactory; @@ -38,6 +62,7 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory _publicAccessService = publicAccessService; _timeProvider = timeProvider; _idKeyMap = idKeyMap; + _signProviderCollection = signProviderCollection; } public async Task CreatePublishedResponseModelAsync(IContent content) @@ -89,6 +114,8 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory responseModel.Variants = CreateVariantsItemResponseModels(entity); + PopulateSignsOnDocuments(responseModel); + return responseModel; } @@ -109,23 +136,29 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory { if (entity.Variations.VariesByCulture() is false) { - yield return new() + var model = new DocumentVariantItemResponseModel() { Name = entity.Name ?? string.Empty, State = DocumentVariantStateHelper.GetState(entity, null), Culture = null, }; + + PopulateSignsOnVariants(model); + yield return model; yield break; } foreach (KeyValuePair cultureNamePair in entity.CultureNames) { - yield return new() + var model = new DocumentVariantItemResponseModel() { Name = cultureNamePair.Value, Culture = cultureNamePair.Key, State = DocumentVariantStateHelper.GetState(entity, cultureNamePair.Key) }; + + PopulateSignsOnVariants(model); + yield return model; } } @@ -178,4 +211,20 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory return Attempt.SucceedWithStatus(ContentPublishingOperationStatus.Success, model); } + + private void PopulateSignsOnDocuments(DocumentItemResponseModel model) + { + foreach (ISignProvider signProvider in _signProviderCollection.Where(x => x.CanProvideSigns())) + { + signProvider.PopulateSignsAsync([model]).GetAwaiter().GetResult(); + } + } + + private void PopulateSignsOnVariants(DocumentVariantItemResponseModel model) + { + foreach (ISignProvider signProvider in _signProviderCollection.Where(x => x.CanProvideSigns())) + { + signProvider.PopulateSignsAsync([model]).GetAwaiter().GetResult(); + } + } } diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs index 3fd3fe6a7c..06be1b91ff 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs @@ -1,4 +1,6 @@ -using Umbraco.Cms.Api.Management.ViewModels.Content; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; @@ -12,30 +14,55 @@ public abstract class ContentMapDefinition _propertyEditorCollection = propertyEditorCollection; + protected ContentMapDefinition( + PropertyEditorCollection propertyEditorCollection, + IDataValueEditorFactory dataValueEditorFactory) + { + _propertyEditorCollection = propertyEditorCollection; + _dataValueEditorFactory = dataValueEditorFactory; + } + + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 18.")] + protected ContentMapDefinition(PropertyEditorCollection propertyEditorCollection) + : this( + propertyEditorCollection, + StaticServiceProvider.Instance.GetRequiredService()) + { + } protected delegate void ValueViewModelMapping(IDataEditor propertyEditor, TValueViewModel variantViewModel); protected delegate void VariantViewModelMapping(string? culture, string? segment, TVariantViewModel variantViewModel); - protected IEnumerable MapValueViewModels(IEnumerable properties, ValueViewModelMapping? additionalPropertyMapping = null, bool published = false) => - properties + protected IEnumerable MapValueViewModels( + IEnumerable properties, + ValueViewModelMapping? additionalPropertyMapping = null, + bool published = false) + { + Dictionary missingPropertyEditors = []; + return properties .SelectMany(property => property .Values .Select(propertyValue => { IDataEditor? propertyEditor = _propertyEditorCollection[property.PropertyType.PropertyEditorAlias]; - if (propertyEditor == null) + if (propertyEditor is null && !missingPropertyEditors.TryGetValue(property.PropertyType.PropertyEditorAlias, out propertyEditor)) { - return null; + // We cache the missing property editors to avoid creating multiple instances of them + propertyEditor = new MissingPropertyEditor(property.PropertyType.PropertyEditorAlias, _dataValueEditorFactory); + missingPropertyEditors[property.PropertyType.PropertyEditorAlias] = propertyEditor; } IProperty? publishedProperty = null; if (published) { publishedProperty = new Property(property.PropertyType); - publishedProperty.SetValue(propertyValue.PublishedValue, propertyValue.Culture, propertyValue.Segment); + publishedProperty.SetValue( + propertyValue.PublishedValue, + propertyValue.Culture, + propertyValue.Segment); } var variantViewModel = new TValueViewModel @@ -43,14 +70,18 @@ public abstract class ContentMapDefinition MapVariantViewModels(TContent source, VariantViewModelMapping? additionalVariantMapping = null) { diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs index 0960b2d72a..fa703c134d 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs @@ -1,8 +1,10 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Mapping.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; using Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint; using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Mapping; @@ -15,8 +17,23 @@ public class DocumentMapDefinition : ContentMapDefinition _commonMapper = commonMapper; + public DocumentMapDefinition( + PropertyEditorCollection propertyEditorCollection, + CommonMapper commonMapper, + IDataValueEditorFactory dataValueEditorFactory) + : base(propertyEditorCollection, dataValueEditorFactory) + => _commonMapper = commonMapper; + + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 18.")] + public DocumentMapDefinition( + PropertyEditorCollection propertyEditorCollection, + CommonMapper commonMapper) + : this( + propertyEditorCollection, + commonMapper, + StaticServiceProvider.Instance.GetRequiredService()) + { + } public void DefineMaps(IUmbracoMapper mapper) { diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs index 5e12e245bd..0012d244a7 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs @@ -1,7 +1,9 @@ -using Umbraco.Cms.Api.Management.Mapping.Content; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Mapping.Content; using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; @@ -11,8 +13,19 @@ namespace Umbraco.Cms.Api.Management.Mapping.Document; public class DocumentVersionMapDefinition : ContentMapDefinition, IMapDefinition { - public DocumentVersionMapDefinition(PropertyEditorCollection propertyEditorCollection) - : base(propertyEditorCollection) + public DocumentVersionMapDefinition( + PropertyEditorCollection propertyEditorCollection, + IDataValueEditorFactory dataValueEditorFactory) + : base(propertyEditorCollection, dataValueEditorFactory) + { + } + + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 18.")] + public DocumentVersionMapDefinition( + PropertyEditorCollection propertyEditorCollection) + : this( + propertyEditorCollection, + StaticServiceProvider.Instance.GetRequiredService()) { } diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Installer/InstallerViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Installer/InstallerViewModelsMapDefinition.cs index 466407a818..06c52dd559 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Installer/InstallerViewModelsMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Installer/InstallerViewModelsMapDefinition.cs @@ -119,6 +119,7 @@ public class InstallerViewModelsMapDefinition : IMapDefinition target.ServerPlaceholder = source.ServerPlaceholder ?? string.Empty; target.SortOrder = source.SortOrder; target.SupportsIntegratedAuthentication = source.SupportsIntegratedAuthentication; + target.SupportsTrustServerCertificate = source.SupportsTrustServerCertificate; target.IsConfigured = false; // Defaults to false, we'll set this to true if needed, } @@ -136,6 +137,7 @@ public class InstallerViewModelsMapDefinition : IMapDefinition target.ServerPlaceholder = source.ServerPlaceholder; target.SortOrder = source.SortOrder; target.SupportsIntegratedAuthentication = source.SupportsIntegratedAuthentication; + target.SupportsTrustServerCertificate = source.SupportsTrustServerCertificate; } // Umbraco.Code.MapAll diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs index 0f0e01b597..8bd6284b18 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs @@ -1,7 +1,9 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Mapping.Content; using Umbraco.Cms.Api.Management.ViewModels.Media; using Umbraco.Cms.Api.Management.ViewModels.Media.Collection; using Umbraco.Cms.Api.Management.ViewModels.MediaType; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Mapping; @@ -14,10 +16,24 @@ public class MediaMapDefinition : ContentMapDefinition _commonMapper = commonMapper; + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 18.")] + public MediaMapDefinition( + PropertyEditorCollection propertyEditorCollection, + CommonMapper commonMapper) + : this( + propertyEditorCollection, + commonMapper, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + public void DefineMaps(IUmbracoMapper mapper) { mapper.Define((_, _) => new MediaResponseModel(), Map); diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs index 64bc0c1e40..10e750cf9d 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs @@ -1,6 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Mapping.Content; using Umbraco.Cms.Api.Management.ViewModels.Member; using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; @@ -9,8 +11,19 @@ namespace Umbraco.Cms.Api.Management.Mapping.Member; public class MemberMapDefinition : ContentMapDefinition, IMapDefinition { - public MemberMapDefinition(PropertyEditorCollection propertyEditorCollection) - : base(propertyEditorCollection) + public MemberMapDefinition( + PropertyEditorCollection propertyEditorCollection, + IDataValueEditorFactory dataValueEditorFactory) + : base(propertyEditorCollection, dataValueEditorFactory) + { + } + + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 18.")] + public MemberMapDefinition( + PropertyEditorCollection propertyEditorCollection) + : this( + propertyEditorCollection, + StaticServiceProvider.Instance.GetRequiredService()) { } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index cbee7b30c4..b0c205c82b 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -37379,7 +37379,8 @@ "requiresServer", "serverPlaceholder", "sortOrder", - "supportsIntegratedAuthentication" + "supportsIntegratedAuthentication", + "supportsTrustServerCertificate" ], "type": "object", "properties": { @@ -37419,6 +37420,9 @@ "supportsIntegratedAuthentication": { "type": "boolean" }, + "supportsTrustServerCertificate": { + "type": "boolean" + }, "requiresConnectionTest": { "type": "boolean" } diff --git a/src/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProvider.cs b/src/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProvider.cs index 50c421a792..e7563aa310 100644 --- a/src/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProvider.cs +++ b/src/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProvider.cs @@ -1,8 +1,5 @@ using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Api.Management.ViewModels.Document; -using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; -using Umbraco.Cms.Api.Management.ViewModels.Document.Item; -using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; namespace Umbraco.Cms.Api.Management.Services.Signs; @@ -17,15 +14,15 @@ public class HasPendingChangesSignProvider : ISignProvider /// public bool CanProvideSigns() where TItem : IHasSigns => - typeof(TItem) == typeof(DocumentTreeItemResponseModel) || - typeof(TItem) == typeof(DocumentCollectionResponseModel) || - typeof(TItem) == typeof(DocumentItemResponseModel); + typeof(TItem) == typeof(DocumentVariantItemResponseModel) || + typeof(TItem) == typeof(DocumentVariantResponseModel); + /// - public Task PopulateSignsAsync(IEnumerable itemViewModels) + public Task PopulateSignsAsync(IEnumerable items) where TItem : IHasSigns { - foreach (TItem item in itemViewModels) + foreach (TItem item in items) { if (HasPendingChanges(item)) { @@ -39,11 +36,10 @@ public class HasPendingChangesSignProvider : ISignProvider /// /// Determines if the given item has any variant that has pending changes. /// - private bool HasPendingChanges(object item) => item switch + private static bool HasPendingChanges(object item) => item switch { - DocumentTreeItemResponseModel { Variants: var v } when v.Any(x => x.State == DocumentVariantState.PublishedPendingChanges) => true, - DocumentCollectionResponseModel { Variants: var v } when v.Any(x => x.State == DocumentVariantState.PublishedPendingChanges) => true, - DocumentItemResponseModel { Variants: var v } when v.Any(x => x.State == DocumentVariantState.PublishedPendingChanges) => true, + DocumentVariantItemResponseModel variant => variant.State == DocumentVariantState.PublishedPendingChanges, + DocumentVariantResponseModel variant => variant.State == DocumentVariantState.PublishedPendingChanges, _ => false, }; } diff --git a/src/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProvider.cs b/src/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProvider.cs index 599d10ae67..e9f949a57a 100644 --- a/src/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProvider.cs +++ b/src/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProvider.cs @@ -1,7 +1,10 @@ using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; using Umbraco.Cms.Api.Management.ViewModels.Document.Item; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Constants = Umbraco.Cms.Core.Constants; @@ -15,11 +18,16 @@ internal class HasScheduleSignProvider : ISignProvider private const string Alias = Constants.Conventions.Signs.Prefix + "ScheduledForPublish"; private readonly IContentService _contentService; + private readonly IIdKeyMap _keyMap; /// /// Initializes a new instance of the class. /// - public HasScheduleSignProvider(IContentService contentService) => _contentService = contentService; + public HasScheduleSignProvider(IContentService contentService, IIdKeyMap keyMap) + { + _contentService = contentService; + _keyMap = keyMap; + } /// public bool CanProvideSigns() @@ -29,15 +37,89 @@ internal class HasScheduleSignProvider : ISignProvider typeof(TItem) == typeof(DocumentItemResponseModel); /// - public Task PopulateSignsAsync(IEnumerable itemViewModels) + public Task PopulateSignsAsync(IEnumerable items) where TItem : IHasSigns { - IEnumerable contentKeysScheduledForPublishing = _contentService.GetScheduledContentKeys(itemViewModels.Select(x => x.Id)); - foreach (Guid key in contentKeysScheduledForPublishing) + IDictionary> schedules = _contentService.GetContentSchedulesByIds(items.Select(x => x.Id).ToArray()); + foreach (TItem item in items) { - itemViewModels.First(x => x.Id == key).AddSign(Alias); + Attempt itemId = _keyMap.GetIdForKey(item.Id, UmbracoObjectTypes.Document); + if (itemId.Success is false) + { + continue; + } + + if (!schedules.TryGetValue(itemId.Result, out IEnumerable? contentSchedules)) + { + continue; + } + + switch (item) + { + case DocumentTreeItemResponseModel documentTreeItemResponseModel: + documentTreeItemResponseModel.Variants = PopulateVariants(documentTreeItemResponseModel.Variants, contentSchedules); + break; + + case DocumentCollectionResponseModel documentCollectionResponseModel: + documentCollectionResponseModel.Variants = PopulateVariants(documentCollectionResponseModel.Variants, contentSchedules); + break; + + case DocumentItemResponseModel documentItemResponseModel: + documentItemResponseModel.Variants = PopulateVariants(documentItemResponseModel.Variants, contentSchedules); + break; + } } return Task.CompletedTask; } + + private IEnumerable PopulateVariants( + IEnumerable variants, IEnumerable schedules) + { + DocumentVariantItemResponseModel[] variantsArray = variants.ToArray(); + if (variantsArray.Length == 1) + { + DocumentVariantItemResponseModel variant = variantsArray[0]; + variant.AddSign(Alias); + return variantsArray; + } + + foreach (DocumentVariantItemResponseModel variant in variantsArray) + { + ContentSchedule? schedule = schedules.FirstOrDefault(x => x.Culture == variant.Culture); + bool isScheduled = schedule != null && schedule.Date > DateTime.Now && string.Equals(schedule.Culture, variant.Culture); + + if (isScheduled) + { + variant.AddSign(Alias); + } + } + + return variantsArray; + } + + private IEnumerable PopulateVariants( + IEnumerable variants, IEnumerable schedules) + { + DocumentVariantResponseModel[] variantsArray = variants.ToArray(); + if (variantsArray.Length == 1) + { + DocumentVariantResponseModel variant = variantsArray[0]; + variant.AddSign(Alias); + return variantsArray; + } + + foreach (DocumentVariantResponseModel variant in variantsArray) + { + ContentSchedule? schedule = schedules.FirstOrDefault(x => x.Culture == variant.Culture); + bool isScheduled = schedule != null && schedule.Date > DateTime.Now && string.Equals(schedule.Culture, variant.Culture); + + if (isScheduled) + { + variant.AddSign(Alias); + } + } + + return variantsArray; + } } diff --git a/src/Umbraco.Cms.Api.Management/Services/Signs/ISignProvider.cs b/src/Umbraco.Cms.Api.Management/Services/Signs/ISignProvider.cs index e0324f05c8..e47a128e74 100644 --- a/src/Umbraco.Cms.Api.Management/Services/Signs/ISignProvider.cs +++ b/src/Umbraco.Cms.Api.Management/Services/Signs/ISignProvider.cs @@ -18,7 +18,6 @@ public interface ISignProvider /// Populates the provided item view models with signs. /// /// Type of item view model supporting signs. - /// The collection of item view models to be populated with signs. - Task PopulateSignsAsync(IEnumerable itemViewModels) + Task PopulateSignsAsync(IEnumerable items) where TItem : IHasSigns; } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantItemResponseModel.cs index ccdc16ec0d..3e4d8913f8 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantItemResponseModel.cs @@ -2,7 +2,25 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Document; -public class DocumentVariantItemResponseModel : VariantItemResponseModelBase +public class DocumentVariantItemResponseModel : VariantItemResponseModelBase, IHasSigns { + private readonly List _signs = []; + + public Guid Id { get; } + + public IEnumerable Signs + { + get => _signs.AsEnumerable(); + set + { + _signs.Clear(); + _signs.AddRange(value); + } + } + + public void AddSign(string alias) => _signs.Add(new SignModel { Alias = alias }); + + public void RemoveSign(string alias) => _signs.RemoveAll(x => x.Alias == alias); + public required DocumentVariantState State { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs index b6990c1b3c..cfa3ef5809 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs @@ -2,7 +2,7 @@ using Umbraco.Cms.Api.Management.ViewModels.Content; namespace Umbraco.Cms.Api.Management.ViewModels.Document; -public class DocumentVariantResponseModel : VariantResponseModelBase +public class DocumentVariantResponseModel : VariantResponseModelBase, IHasSigns { public DocumentVariantState State { get; set; } @@ -11,4 +11,22 @@ public class DocumentVariantResponseModel : VariantResponseModelBase public DateTimeOffset? ScheduledPublishDate { get; set; } public DateTimeOffset? ScheduledUnpublishDate { get; set; } + + private readonly List _signs = []; + + public Guid Id { get; } + + public IEnumerable Signs + { + get => _signs.AsEnumerable(); + set + { + _signs.Clear(); + _signs.AddRange(value); + } + } + + public void AddSign(string alias) => _signs.Add(new SignModel { Alias = alias }); + + public void RemoveSign(string alias) => _signs.RemoveAll(x => x.Alias == alias); } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Installer/DatabaseSettingsPresentationModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Installer/DatabaseSettingsPresentationModel.cs index cb1fe33a4f..ad1536baa1 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Installer/DatabaseSettingsPresentationModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Installer/DatabaseSettingsPresentationModel.cs @@ -28,5 +28,7 @@ public class DatabaseSettingsPresentationModel public bool SupportsIntegratedAuthentication { get; set; } + public bool SupportsTrustServerCertificate { get; set; } + public bool RequiresConnectionTest { get; set; } } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs index 746ad2b93e..5ab2d1e7be 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs @@ -45,6 +45,9 @@ public class SqlAzureDatabaseProviderMetadata : IDatabaseProviderMetadata /// public bool SupportsIntegratedAuthentication => false; + /// + public bool SupportsTrustServerCertificate => false; + /// public bool RequiresConnectionTest => true; diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs index 589ef5a623..0dc3921519 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs @@ -45,6 +45,9 @@ public class SqlLocalDbDatabaseProviderMetadata : IDatabaseProviderMetadata /// public bool SupportsIntegratedAuthentication => false; + /// + public bool SupportsTrustServerCertificate => false; + /// public bool RequiresConnectionTest => false; diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs index a6919521b1..23cb1f5c64 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs @@ -46,6 +46,9 @@ public class SqlServerDatabaseProviderMetadata : IDatabaseProviderMetadata /// public bool SupportsIntegratedAuthentication => true; + /// + public bool SupportsTrustServerCertificate => true; + /// public bool RequiresConnectionTest => true; diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs index d087acc8cb..a8c60d255b 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs @@ -42,6 +42,9 @@ public class SqliteDatabaseProviderMetadata : IDatabaseProviderMetadata /// public bool SupportsIntegratedAuthentication => false; + /// + public bool SupportsTrustServerCertificate => false; + /// public bool RequiresConnectionTest => false; diff --git a/src/Umbraco.Core/Models/Installer/DatabaseSettingsModel.cs b/src/Umbraco.Core/Models/Installer/DatabaseSettingsModel.cs index 085dbbfb53..eff2b88313 100644 --- a/src/Umbraco.Core/Models/Installer/DatabaseSettingsModel.cs +++ b/src/Umbraco.Core/Models/Installer/DatabaseSettingsModel.cs @@ -22,5 +22,7 @@ public class DatabaseSettingsModel public bool SupportsIntegratedAuthentication { get; set; } + public bool SupportsTrustServerCertificate { get; set; } + public bool RequiresConnectionTest { get; set; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs index 9ff75c2b3d..6ac6470a85 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; @@ -53,11 +54,11 @@ public interface IDocumentRepository : IContentRepository, IReadR /// /// Gets the content keys from the provided collection of keys that are scheduled for publishing. /// - /// The content keys. + /// The IDs of the documents. /// /// The provided collection of content keys filtered for those that are scheduled for publishing. /// - IEnumerable GetScheduledContentKeys(Guid[] keys) => []; + IDictionary> GetContentSchedulesByIds(int[] documentIds) => ImmutableDictionary>.Empty; /// /// Get the count of published items diff --git a/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs index 2f449666bd..374efd67a7 100644 --- a/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs @@ -1,5 +1,10 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Core.PropertyEditors; @@ -10,19 +15,73 @@ namespace Umbraco.Cms.Core.PropertyEditors; [HideFromTypeFinder] public class MissingPropertyEditor : IDataEditor { - public string Alias => "Umbraco.Missing"; + private const string EditorAlias = "Umbraco.Missing"; + private readonly IDataValueEditorFactory _dataValueEditorFactory; + private IDataValueEditor? _valueEditor; + /// + /// Initializes a new instance of the class. + /// + public MissingPropertyEditor( + string missingEditorAlias, + IDataValueEditorFactory dataValueEditorFactory) + { + _dataValueEditorFactory = dataValueEditorFactory; + Alias = missingEditorAlias; + } + + [Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in Umbraco 18.")] + public MissingPropertyEditor() + : this( + EditorAlias, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + /// + public string Alias { get; } + + /// + /// Gets the name of the editor. + /// public string Name => "Missing property editor"; + /// public bool IsDeprecated => false; - public IDictionary DefaultConfiguration => throw new NotImplementedException(); + /// + public bool SupportsReadOnly => true; - public IPropertyIndexValueFactory PropertyIndexValueFactory => throw new NotImplementedException(); + /// + public IDictionary DefaultConfiguration => new Dictionary(); + /// + public IPropertyIndexValueFactory PropertyIndexValueFactory => new DefaultPropertyIndexValueFactory(); + + /// + public IDataValueEditor GetValueEditor() => _valueEditor + ??= _dataValueEditorFactory.Create( + new DataEditorAttribute(EditorAlias)); + + /// + public IDataValueEditor GetValueEditor(object? configurationObject) => GetValueEditor(); + + /// public IConfigurationEditor GetConfigurationEditor() => new ConfigurationEditor(); - public IDataValueEditor GetValueEditor() => throw new NotImplementedException(); + // provides the property value editor + internal sealed class MissingPropertyValueEditor : DataValueEditor + { + public MissingPropertyValueEditor( + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute) + : base(shortStringHelper, jsonSerializer, ioHelper, attribute) + { + } - public IDataValueEditor GetValueEditor(object? configurationObject) => throw new NotImplementedException(); + /// + public override bool IsReadOnly => true; + } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MissingPropertyEditorValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MissingPropertyEditorValueConverter.cs new file mode 100644 index 0000000000..da70e558cf --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MissingPropertyEditorValueConverter.cs @@ -0,0 +1,21 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +/// +/// A value converter for the missing property editor, which always returns a string. +/// +[DefaultPropertyValueConverter] +public class MissingPropertyEditorValueConverter : PropertyValueConverterBase +{ + public override bool IsConverter(IPublishedPropertyType propertyType) + => "Umb.PropertyEditorUi.Missing".Equals(propertyType.EditorUiAlias); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof(string); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source?.ToString() ?? string.Empty; +} diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index f3ae553ce4..139b057ba0 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Microsoft.Extensions.DependencyInjection; @@ -1017,18 +1018,29 @@ public class ContentService : RepositoryService, IContentService /// - public IEnumerable GetScheduledContentKeys(IEnumerable keys) + public IDictionary> GetContentSchedulesByIds(Guid[] keys) { - Guid[] idsA = keys.ToArray(); - if (idsA.Length == 0) + if (keys.Length == 0) { - return Enumerable.Empty(); + return ImmutableDictionary>.Empty; + } + + List contentIds = []; + foreach (var key in keys) + { + Attempt contentId = _idKeyMap.GetIdForKey(key, UmbracoObjectTypes.Document); + if (contentId.Success is false) + { + continue; + } + + contentIds.Add(contentId.Result); } using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.GetScheduledContentKeys(idsA); + return _documentRepository.GetContentSchedulesByIds(contentIds.ToArray()); } } diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index 6ba02cc880..fe779507c4 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -440,7 +440,7 @@ public class EntityService : RepositoryService, IEntityService if (take == 0) { - totalRecords = CountChildren(parentId, childObjectType, filter); + totalRecords = CountChildren(parentId, childObjectType, filter: filter); return Enumerable.Empty(); } @@ -487,6 +487,12 @@ public class EntityService : RepositoryService, IEntityService return Enumerable.Empty(); } + if (take == 0) + { + totalRecords = CountChildren(parentId, objectType, true, filter); + return Enumerable.Empty(); + } + PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize); IEnumerable children = GetPagedChildren( @@ -693,17 +699,18 @@ public class EntityService : RepositoryService, IEntityService } } - private int CountChildren(int id, UmbracoObjectTypes objectType, IQuery? filter = null) => - CountChildren(id, new HashSet() { objectType }, filter); + private int CountChildren(int id, UmbracoObjectTypes objectType, bool trashed = false, IQuery? filter = null) => + CountChildren(id, new HashSet() { objectType }, trashed, filter); private int CountChildren( int id, IEnumerable objectTypes, + bool trashed = false, IQuery? filter = null) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - IQuery query = Query().Where(x => x.ParentId == id && x.Trashed == false); + IQuery query = Query().Where(x => x.ParentId == id && x.Trashed == trashed); var objectTypeGuids = objectTypes.Select(x => x.GetGuid()).ToHashSet(); return _entityRepository.CountByQuery(query, objectTypeGuids, filter); @@ -782,7 +789,7 @@ public class EntityService : RepositoryService, IEntityService if (take == 0) { - totalRecords = CountChildren(parentId, childObjectTypes, filter); + totalRecords = CountChildren(parentId, childObjectTypes, filter: filter); return Array.Empty(); } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 1307347557..c79260ecaa 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; @@ -276,13 +277,14 @@ public interface IContentService : IContentServiceBase bool HasChildren(int id); /// - /// Gets the content keys from the provided collection of keys that are scheduled for publishing. + /// Gets a dictionary of content Ids and their matching content schedules. /// /// The content keys. /// - /// The provided collection of content keys filtered for those that are scheduled for publishing. + /// A dictionary with a nodeId and an IEnumerable of matching ContentSchedules. /// - IEnumerable GetScheduledContentKeys(IEnumerable keys) => []; + IDictionary> GetContentSchedulesByIds(Guid[] keys) => ImmutableDictionary>.Empty; + #endregion diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 33e5883863..a3b5f3113a 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -53,6 +53,7 @@ using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; +using Umbraco.Cms.Infrastructure.Persistence.Relations; using Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers; using Umbraco.Cms.Infrastructure.Routing; using Umbraco.Cms.Infrastructure.Runtime; @@ -434,6 +435,13 @@ public static partial class UmbracoBuilderExtensions .AddNotificationAsyncHandler() .AddNotificationAsyncHandler(); + // Handles for relation persistence on content save. + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); + return builder; } diff --git a/src/Umbraco.Infrastructure/Extensions/RichTextEditorValueExtensions.cs b/src/Umbraco.Infrastructure/Extensions/RichTextEditorValueExtensions.cs index 3cc16b6c21..3a456bb8a8 100644 --- a/src/Umbraco.Infrastructure/Extensions/RichTextEditorValueExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/RichTextEditorValueExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Models; @@ -31,8 +32,30 @@ internal static class RichTextEditorValueExtensions { foreach (BlockPropertyValue item in dataItem.Values) { - item.PropertyType = elementTypes.FirstOrDefault(x => x.Key == dataItem.ContentTypeKey)?.PropertyTypes.FirstOrDefault(pt => pt.Alias == item.Alias); + if (TryResolvePropertyType(elementTypes, dataItem.ContentTypeKey, item.Alias, out IPropertyType? resolvedPropertyType)) + { + item.PropertyType = resolvedPropertyType; + } } } } + + private static bool TryResolvePropertyType(IEnumerable elementTypes, Guid contentTypeKey, string propertyTypeAlias, [NotNullWhen(true)] out IPropertyType? propertyType) + { + IContentType? elementType = elementTypes.FirstOrDefault(x => x.Key == contentTypeKey); + if (elementType is null) + { + propertyType = null; + return false; + } + + propertyType = elementType.PropertyTypes.FirstOrDefault(pt => pt.Alias == propertyTypeAlias); + if (propertyType is not null) + { + return true; + } + + propertyType = elementType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propertyTypeAlias); + return propertyType is not null; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs b/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs index f266df71ff..6eb87c29f2 100644 --- a/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs +++ b/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs @@ -42,6 +42,9 @@ public class CustomConnectionStringDatabaseProviderMetadata : IDatabaseProviderM /// public bool SupportsIntegratedAuthentication => false; + /// + public bool SupportsTrustServerCertificate => false; + /// public bool RequiresConnectionTest => true; diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs index 005345b40f..bbc93b0fba 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs @@ -58,6 +58,7 @@ public static class DatabaseProviderMetadataExtensions Server = server ?? string.Empty, Login = login ?? string.Empty, Password = password ?? string.Empty, - IntegratedAuth = integratedAuth == true && databaseProviderMetadata.SupportsIntegratedAuthentication + IntegratedAuth = integratedAuth == true && databaseProviderMetadata.SupportsIntegratedAuthentication, + TrustServerCertificate = databaseProviderMetadata.SupportsTrustServerCertificate, }); } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs index 68c6a40946..4d22708b6a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs @@ -11,17 +11,23 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories; internal static class DataTypeFactory { - public static IDataType BuildEntity(DataTypeDto dto, PropertyEditorCollection editors, ILogger logger, IConfigurationEditorJsonSerializer serializer) + public static IDataType BuildEntity( + DataTypeDto dto, + PropertyEditorCollection editors, + ILogger logger, + IConfigurationEditorJsonSerializer serializer, + IDataValueEditorFactory dataValueEditorFactory) { // Check we have an editor for the data type. if (!editors.TryGet(dto.EditorAlias, out IDataEditor? editor)) { logger.LogWarning( - "Could not find an editor with alias {EditorAlias}, treating as Label. " + "The site may fail to boot and/or load data types and run.", dto.EditorAlias); - - // Create as special type, which downstream can be handled by converting to a LabelPropertyEditor to make clear - // the situation to the user. - editor = new MissingPropertyEditor(); + "Could not find an editor with alias {EditorAlias}, treating as Missing. " + "The site may fail to boot and/or load data types and run.", + dto.EditorAlias); + editor = + new MissingPropertyEditor( + dto.EditorAlias, + dataValueEditorFactory); } var dataType = new DataType(editor, serializer); @@ -42,7 +48,7 @@ internal static class DataTypeFactory dataType.SortOrder = dto.NodeDto.SortOrder; dataType.Trashed = dto.NodeDto.Trashed; dataType.CreatorId = dto.NodeDto.UserId ?? Constants.Security.UnknownUserId; - dataType.EditorUiAlias = dto.EditorUiAlias; + dataType.EditorUiAlias = editor is MissingPropertyEditor ? "Umb.PropertyEditorUi.Missing" : dto.EditorUiAlias; dataType.SetConfigurationData(editor.GetConfigurationEditor().FromDatabase(dto.Configuration, serializer)); diff --git a/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs index 55a7c3a686..9097aeb67f 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs @@ -71,6 +71,12 @@ public interface IDatabaseProviderMetadata [DataMember(Name = "supportsIntegratedAuthentication")] bool SupportsIntegratedAuthentication { get; } + /// + /// Gets a value indicating whether "Trust the database certificate" is supported (e.g. SQL Server & Oracle). + /// + [DataMember(Name = "supportsTrustServerCertificate")] + bool SupportsTrustServerCertificate => false; + /// /// Gets a value indicating whether the connection should be tested before continuing install process. /// diff --git a/src/Umbraco.Infrastructure/Persistence/Relations/ContentRelationsUpdate.cs b/src/Umbraco.Infrastructure/Persistence/Relations/ContentRelationsUpdate.cs new file mode 100644 index 0000000000..871f530a1e --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Relations/ContentRelationsUpdate.cs @@ -0,0 +1,171 @@ +using Microsoft.Extensions.Logging; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence.Querying; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Relations; + +/// +/// Defines a notification handler for content saved operations that persists relations. +/// +internal sealed class ContentRelationsUpdate : + IDistributedCacheNotificationHandler, + IDistributedCacheNotificationHandler, + IDistributedCacheNotificationHandler, + IDistributedCacheNotificationHandler +{ + private readonly IScopeProvider _scopeProvider; + private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories; + private readonly PropertyEditorCollection _propertyEditors; + private readonly IRelationRepository _relationRepository; + private readonly IRelationTypeRepository _relationTypeRepository; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + public ContentRelationsUpdate( + IScopeProvider scopeProvider, + DataValueReferenceFactoryCollection dataValueReferenceFactories, + PropertyEditorCollection propertyEditors, + IRelationRepository relationRepository, + IRelationTypeRepository relationTypeRepository, + ILogger logger) + { + _scopeProvider = scopeProvider; + _dataValueReferenceFactories = dataValueReferenceFactories; + _propertyEditors = propertyEditors; + _relationRepository = relationRepository; + _relationTypeRepository = relationTypeRepository; + _logger = logger; + } + + /// + public void Handle(ContentSavedNotification notification) => PersistRelations(notification.SavedEntities); + + /// + public void Handle(IEnumerable notifications) => PersistRelations(notifications.SelectMany(x => x.SavedEntities)); + + /// + public void Handle(ContentPublishedNotification notification) => PersistRelations(notification.PublishedEntities); + + /// + public void Handle(IEnumerable notifications) => PersistRelations(notifications.SelectMany(x => x.PublishedEntities)); + + /// + public void Handle(MediaSavedNotification notification) => PersistRelations(notification.SavedEntities); + + /// + public void Handle(IEnumerable notifications) => PersistRelations(notifications.SelectMany(x => x.SavedEntities)); + + /// + public void Handle(MemberSavedNotification notification) => PersistRelations(notification.SavedEntities); + + /// + public void Handle(IEnumerable notifications) => PersistRelations(notifications.SelectMany(x => x.SavedEntities)); + + private void PersistRelations(IEnumerable entities) + { + using IScope scope = _scopeProvider.CreateScope(); + foreach (IContentBase entity in entities) + { + PersistRelations(scope, entity); + } + + scope.Complete(); + } + + private void PersistRelations(IScope scope, IContentBase entity) + { + // Get all references and automatic relation type aliases. + ISet references = _dataValueReferenceFactories.GetAllReferences(entity.Properties, _propertyEditors); + ISet automaticRelationTypeAliases = _dataValueReferenceFactories.GetAllAutomaticRelationTypesAliases(_propertyEditors); + + if (references.Count == 0) + { + // Delete all relations using the automatic relation type aliases. + _relationRepository.DeleteByParent(entity.Id, automaticRelationTypeAliases.ToArray()); + + // No need to add new references/relations + return; + } + + // Lookup all relation type IDs. + var relationTypeLookup = _relationTypeRepository.GetMany(Array.Empty()) + .Where(x => automaticRelationTypeAliases.Contains(x.Alias)) + .ToDictionary(x => x.Alias, x => x.Id); + + // Lookup node IDs for all GUID based UDIs. + IEnumerable keys = references.Select(x => x.Udi).OfType().Select(x => x.Guid); + var keysLookup = scope.Database.FetchByGroups(keys, Constants.Sql.MaxParameterCount, guids => + { + return scope.SqlContext.Sql() + .Select(x => x.NodeId, x => x.UniqueId) + .From() + .WhereIn(x => x.UniqueId, guids); + }).ToDictionary(x => x.UniqueId, x => x.NodeId); + + // Get all valid relations. + var relations = new List<(int ChildId, int RelationTypeId)>(references.Count); + foreach (UmbracoEntityReference reference in references) + { + if (string.IsNullOrEmpty(reference.RelationTypeAlias)) + { + // Reference does not specify a relation type alias, so skip adding a relation. + _logger.LogDebug("The reference to {Udi} does not specify a relation type alias, so it will not be saved as relation.", reference.Udi); + } + else if (!automaticRelationTypeAliases.Contains(reference.RelationTypeAlias)) + { + // Returning a reference that doesn't use an automatic relation type is an issue that should be fixed in code. + _logger.LogError("The reference to {Udi} uses a relation type {RelationTypeAlias} that is not an automatic relation type.", reference.Udi, reference.RelationTypeAlias); + } + else if (!relationTypeLookup.TryGetValue(reference.RelationTypeAlias, out int relationTypeId)) + { + // A non-existent relation type could be caused by an environment issue (e.g. it was manually removed). + _logger.LogWarning("The reference to {Udi} uses a relation type {RelationTypeAlias} that does not exist.", reference.Udi, reference.RelationTypeAlias); + } + else if (reference.Udi is not GuidUdi udi || !keysLookup.TryGetValue(udi.Guid, out var id)) + { + // Relations only support references to items that are stored in the NodeDto table (because of foreign key constraints). + _logger.LogInformation("The reference to {Udi} can not be saved as relation, because it doesn't have a node ID.", reference.Udi); + } + else + { + relations.Add((id, relationTypeId)); + } + } + + // Get all existing relations (optimize for adding new and keeping existing relations). + IQuery query = scope.SqlContext.Query().Where(x => x.ParentId == entity.Id).WhereIn(x => x.RelationTypeId, relationTypeLookup.Values); + var existingRelations = _relationRepository.GetPagedRelationsByQuery(query, 0, int.MaxValue, out _, null) + .ToDictionary(x => (x.ChildId, x.RelationTypeId)); // Relations are unique by parent ID, child ID and relation type ID. + + // Add relations that don't exist yet. + IEnumerable relationsToAdd = relations.Except(existingRelations.Keys).Select(x => new ReadOnlyRelation(entity.Id, x.ChildId, x.RelationTypeId)); + _relationRepository.SaveBulk(relationsToAdd); + + // Delete relations that don't exist anymore. + foreach (IRelation relation in existingRelations.Where(x => !relations.Contains(x.Key)).Select(x => x.Value)) + { + _relationRepository.Delete(relation); + } + } + + private sealed class NodeIdKey + { + [Column("id")] + public int NodeId { get; set; } + + [Column("uniqueId")] + public Guid UniqueId { get; set; } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index d90bd3d8c3..457271c1c9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -6,7 +6,6 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Editors; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; @@ -1080,81 +1079,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement #endregion + [Obsolete("This method is no longer used as the persistance of relations has been moved to the ContentRelationsUpdate notification handler. Scheduled for removal in Umbraco 18.")] protected void PersistRelations(TEntity entity) - { - // Get all references and automatic relation type aliases - ISet references = _dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors); - ISet automaticRelationTypeAliases = _dataValueReferenceFactories.GetAllAutomaticRelationTypesAliases(PropertyEditors); - - if (references.Count == 0) - { - // Delete all relations using the automatic relation type aliases - RelationRepository.DeleteByParent(entity.Id, automaticRelationTypeAliases.ToArray()); - - // No need to add new references/relations - return; - } - - // Lookup all relation type IDs - var relationTypeLookup = RelationTypeRepository.GetMany(Array.Empty()) - .Where(x => automaticRelationTypeAliases.Contains(x.Alias)) - .ToDictionary(x => x.Alias, x => x.Id); - - // Lookup node IDs for all GUID based UDIs - IEnumerable keys = references.Select(x => x.Udi).OfType().Select(x => x.Guid); - var keysLookup = Database.FetchByGroups(keys, Constants.Sql.MaxParameterCount, guids => - { - return Sql() - .Select(x => x.NodeId, x => x.UniqueId) - .From() - .WhereIn(x => x.UniqueId, guids); - }).ToDictionary(x => x.UniqueId, x => x.NodeId); - - // Get all valid relations - var relations = new List<(int ChildId, int RelationTypeId)>(references.Count); - foreach (UmbracoEntityReference reference in references) - { - if (string.IsNullOrEmpty(reference.RelationTypeAlias)) - { - // Reference does not specify a relation type alias, so skip adding a relation - Logger.LogDebug("The reference to {Udi} does not specify a relation type alias, so it will not be saved as relation.", reference.Udi); - } - else if (!automaticRelationTypeAliases.Contains(reference.RelationTypeAlias)) - { - // Returning a reference that doesn't use an automatic relation type is an issue that should be fixed in code - Logger.LogError("The reference to {Udi} uses a relation type {RelationTypeAlias} that is not an automatic relation type.", reference.Udi, reference.RelationTypeAlias); - } - else if (!relationTypeLookup.TryGetValue(reference.RelationTypeAlias, out int relationTypeId)) - { - // A non-existent relation type could be caused by an environment issue (e.g. it was manually removed) - Logger.LogWarning("The reference to {Udi} uses a relation type {RelationTypeAlias} that does not exist.", reference.Udi, reference.RelationTypeAlias); - } - else if (reference.Udi is not GuidUdi udi || !keysLookup.TryGetValue(udi.Guid, out var id)) - { - // Relations only support references to items that are stored in the NodeDto table (because of foreign key constraints) - Logger.LogInformation("The reference to {Udi} can not be saved as relation, because doesn't have a node ID.", reference.Udi); - } - else - { - relations.Add((id, relationTypeId)); - } - } - - // Get all existing relations (optimize for adding new and keeping existing relations) - var query = Query().Where(x => x.ParentId == entity.Id).WhereIn(x => x.RelationTypeId, relationTypeLookup.Values); - var existingRelations = RelationRepository.GetPagedRelationsByQuery(query, 0, int.MaxValue, out _, null) - .ToDictionary(x => (x.ChildId, x.RelationTypeId)); // Relations are unique by parent ID, child ID and relation type ID - - // Add relations that don't exist yet - var relationsToAdd = relations.Except(existingRelations.Keys).Select(x => new ReadOnlyRelation(entity.Id, x.ChildId, x.RelationTypeId)); - RelationRepository.SaveBulk(relationsToAdd); - - // Delete relations that don't exist anymore - foreach (IRelation relation in existingRelations.Where(x => !relations.Contains(x.Key)).Select(x => x.Value)) - { - RelationRepository.Delete(relation); - } - } + => Logger.LogWarning("ContentRepositoryBase.PersistRelations was called but this is now an obsolete, no-op method that is unused in Umbraco. No relations were persisted. Relations persistence has moved to the ContentRelationsUpdate notification handler."); /// /// Inserts property values for the content entity @@ -1230,14 +1157,5 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement Database.Execute(SqlContext.Sql().Delete().WhereIn(x => x.Id, existingPropDataIds)); } } - - private sealed class NodeIdKey - { - [Column("id")] - public int NodeId { get; set; } - - [Column("uniqueId")] - public Guid UniqueId { get; set; } - } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs index b58b25f077..3e55ee442f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs @@ -29,6 +29,7 @@ internal sealed class DataTypeRepository : EntityRepositoryBase, private readonly ILogger _dataTypeLogger; private readonly PropertyEditorCollection _editors; private readonly IConfigurationEditorJsonSerializer _serializer; + private readonly IDataValueEditorFactory _dataValueEditorFactory; public DataTypeRepository( IScopeAccessor scopeAccessor, @@ -36,11 +37,13 @@ internal sealed class DataTypeRepository : EntityRepositoryBase, PropertyEditorCollection editors, ILogger logger, ILoggerFactory loggerFactory, - IConfigurationEditorJsonSerializer serializer) + IConfigurationEditorJsonSerializer serializer, + IDataValueEditorFactory dataValueEditorFactory) : base(scopeAccessor, cache, logger) { _editors = editors; _serializer = serializer; + _dataValueEditorFactory = dataValueEditorFactory; _dataTypeLogger = loggerFactory.CreateLogger(); } @@ -262,7 +265,12 @@ internal sealed class DataTypeRepository : EntityRepositoryBase, } List? dtos = Database.Fetch(dataTypeSql); - return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); + return dtos.Select(x => DataTypeFactory.BuildEntity( + x, + _editors, + _dataTypeLogger, + _serializer, + _dataValueEditorFactory)).ToArray(); } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -273,7 +281,12 @@ internal sealed class DataTypeRepository : EntityRepositoryBase, List? dtos = Database.Fetch(sql); - return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); + return dtos.Select(x => DataTypeFactory.BuildEntity( + x, + _editors, + _dataTypeLogger, + _serializer, + _dataValueEditorFactory)).ToArray(); } #endregion diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index e047724e08..21d7986d4b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1073,8 +1073,6 @@ public class DocumentRepository : ContentRepositoryBase - public IEnumerable GetScheduledContentKeys(Guid[] keys) + public IDictionary> GetContentSchedulesByIds(int[] documentIds) { - var action = ContentScheduleAction.Release.ToString(); - DateTime now = DateTime.UtcNow; + Sql sql = Sql() + .Select() + .From() + .WhereIn(contentScheduleDto => contentScheduleDto.NodeId, documentIds); - Sql sql = SqlContext.Sql(); - sql - .Select(x => x.UniqueId) - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .WhereIn(x => x.UniqueId, keys) - .WhereIn(x => x.NodeId, Sql() - .Select(x => x.NodeId) - .From() - .Where(x => x.Action == action && x.Date >= now)); + List? contentScheduleDtos = Database.Fetch(sql); - return Database.Fetch(sql); + IDictionary> dictionary = contentScheduleDtos + .GroupBy(contentSchedule => contentSchedule.NodeId) + .ToDictionary( + group => group.Key, + group => group.Select(scheduleDto => new ContentSchedule( + scheduleDto.Id, + LanguageRepository.GetIsoCodeById(scheduleDto.LanguageId) ?? Constants.System.InvariantCulture, + scheduleDto.Date, + scheduleDto.Action == ContentScheduleAction.Release.ToString() + ? ContentScheduleAction.Release + : ContentScheduleAction.Expire)) + .ToList().AsEnumerable()); // We have to materialize it here, + // to avoid this being used after the scope is disposed. + + return dictionary; } /// diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs index 1b4dd2d8a9..d08607ec09 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -416,8 +416,6 @@ public class MediaRepository : ContentRepositoryBase + public override IValueRequiredValidator RequiredValidator => new FileUploadValueRequiredValidator(); + /// public override object? ToEditor(IProperty property, string? culture = null, string? segment = null) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/Validators/FileUploadValueRequiredValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/Validators/FileUploadValueRequiredValidator.cs new file mode 100644 index 0000000000..7a5e852af6 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/Validators/FileUploadValueRequiredValidator.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Nodes; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.PropertyEditors.Validators; + +namespace Umbraco.Cms.Infrastructure.PropertyEditors.Validators; + +/// +/// Custom validator for block value required validation. +/// +internal sealed class FileUploadValueRequiredValidator : RequiredValidator +{ + /// + public override IEnumerable ValidateRequired(object? value, string? valueType) + { + IEnumerable validationResults = base.ValidateRequired(value, valueType); + + if (value is null) + { + return validationResults; + } + + if (value is JsonObject jsonObject && jsonObject.TryGetPropertyValue("src", out JsonNode? source)) + { + string sourceString = source!.GetValue(); + if (string.IsNullOrEmpty(sourceString)) + { + validationResults = validationResults.Append(new ValidationResult(Constants.Validation.ErrorMessages.Properties.Empty, ["value"])); + } + } + + return validationResults; + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs index 6923110a2b..fbdfe33758 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs @@ -14,7 +14,6 @@ using Umbraco.Cms.Infrastructure.HybridCache.Serialization; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; -using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; @@ -61,7 +60,12 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe /// public async Task DeleteContentItemAsync(int id) - => await Database.ExecuteAsync("DELETE FROM cmsContentNu WHERE nodeId = @id", new { id }); + { + Sql sql = Sql() + .Delete() + .Where(x => x.NodeId == id); + await Database.ExecuteAsync(sql); + } /// public async Task RefreshContentAsync(ContentCacheNode contentCacheNode, PublishedState publishedState) @@ -83,7 +87,10 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe await OnRepositoryRefreshed(serializer, contentCacheNode, false); break; case PublishedState.Unpublishing: - await Database.ExecuteAsync("DELETE FROM cmsContentNu WHERE nodeId = @id AND published = 1", new { id = contentCacheNode.Id }); + Sql sql = Sql() + .Delete() + .Where(x => x.NodeId == contentCacheNode.Id && x.Published); + await Database.ExecuteAsync(sql); break; } } @@ -193,11 +200,11 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe ? SqlMediaSourcesSelect() : throw new ArgumentOutOfRangeException(nameof(objectType), objectType, null); - sql.InnerJoin("n") - .On((n, c) => n.NodeId == c.ContentTypeId, "n", "umbracoContent") - .Append(SqlObjectTypeNotTrashed(SqlContext, objectType)) - .WhereIn(x => x.UniqueId, keys,"n") - .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + sql.InnerJoin("n") + .On((n, c) => n.NodeId == c.ContentTypeId, "n", "umbracoContent") + .Append(SqlObjectTypeNotTrashed(SqlContext, objectType)) + .WhereIn(x => x.UniqueId, keys, "n") + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); return GetContentNodeDtos(sql); } @@ -277,12 +284,13 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe private async Task OnRepositoryRefreshed(IContentCacheDataSerializer serializer, ContentCacheNode content, bool preview) { - ContentNuDto dto = GetDtoFromCacheNode(content, !preview, serializer); + string c(string s) => SqlSyntax.GetQuotedColumnName(s); + await Database.InsertOrUpdateAsync( dto, - "SET data = @data, dataRaw = @dataRaw, rv = rv + 1 WHERE nodeId = @id AND published = @published", + $"SET data = @data, {c("dataRaw")} = @dataRaw, rv = rv + 1 WHERE {c("nodeId")} = @id AND published = @published", new { dataRaw = dto.RawData ?? Array.Empty(), @@ -293,7 +301,7 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe } /// - /// Rebuilds the content database cache for documents. + /// Rebuilds the content database cache for documents by clearing and repopulating the cache with the latest document data. /// /// /// Assumes content tree lock. @@ -308,14 +316,7 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe Guid contentObjectType = Constants.ObjectTypes.Document; // Remove all - if anything fails the transaction will rollback. - if (contentTypeIds.Count == 0) - { - DeleteForObjectType(contentObjectType); - } - else - { - DeleteForObjectTypeAndContentTypes(contentObjectType, contentTypeIds); - } + RemoveByObjectType(contentObjectType, contentTypeIds); // Insert back - if anything fails the transaction will rollback. IQuery query = GetInsertQuery(contentTypeIds); @@ -351,10 +352,10 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe } /// - /// Rebuilds the content database cache for media. + /// Rebuilds the content database cache for media by clearing and repopulating the cache with the latest media data. /// /// - /// Assumes media tree lock. + /// Assumes content tree lock. /// private void RebuildMediaDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection? contentTypeIds) { @@ -366,14 +367,7 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe Guid mediaObjectType = Constants.ObjectTypes.Media; // Remove all - if anything fails the transaction will rollback. - if (contentTypeIds.Count == 0) - { - DeleteForObjectType(mediaObjectType); - } - else - { - DeleteForObjectTypeAndContentTypes(mediaObjectType, contentTypeIds); - } + RemoveByObjectType(mediaObjectType, contentTypeIds); // Insert back - if anything fails the transaction will rollback. IQuery query = GetInsertQuery(contentTypeIds); @@ -394,10 +388,10 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe } /// - /// Rebuilds the content database cache for members. + /// Rebuilds the content database cache for members by clearing and repopulating the cache with the latest member data. /// /// - /// Assumes member tree lock. + /// Assumes content tree lock. /// private void RebuildMemberDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection? contentTypeIds) { @@ -409,14 +403,7 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe Guid memberObjectType = Constants.ObjectTypes.Member; // Remove all - if anything fails the transaction will rollback. - if (contentTypeIds.Count == 0) - { - DeleteForObjectType(memberObjectType); - } - else - { - DeleteForObjectTypeAndContentTypes(memberObjectType, contentTypeIds); - } + RemoveByObjectType(memberObjectType, contentTypeIds); // Insert back - if anything fails the transaction will rollback. IQuery query = GetInsertQuery(contentTypeIds); @@ -435,26 +422,39 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe while (processed < total); } - private void DeleteForObjectType(Guid nodeObjectType) => - Database.Execute( - @" - DELETE FROM cmsContentNu - WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType = @objType - )", - new { objType = nodeObjectType }); - - private void DeleteForObjectTypeAndContentTypes(Guid nodeObjectType, IReadOnlyCollection contentTypeIds) => - Database.Execute( - $@" - DELETE FROM cmsContentNu - WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode - JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id - WHERE umbracoNode.nodeObjectType = @objType - AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) - )", - new { objType = nodeObjectType, ctypes = contentTypeIds }); + private void RemoveByObjectType(Guid objectType, IReadOnlyCollection contentTypeIds) + { + // If the provided contentTypeIds collection is empty, remove all records for the provided object type. + Sql sql; + if (contentTypeIds.Count == 0) + { + sql = Sql() + .Delete() + .WhereIn( + x => x.NodeId, + Sql().Select(n => n.NodeId) + .From() + .Where(n => n.NodeObjectType == objectType)); + Database.Execute(sql); + } + else + { + // Otherwise, if contentTypeIds are provided remove only those records that match the object type and one of the content types. + sql = Sql() + .Delete() + .WhereIn( + x => x.NodeId, + Sql() + .Select(n => n.NodeId) + .From() + .InnerJoin() + .On((n, c) => n.NodeId == c.NodeId) + .Where((n, c) => + n.NodeObjectType == objectType && + contentTypeIds.Contains(c.ContentTypeId))); + Database.Execute(sql); + } + } private IQuery GetInsertQuery(IReadOnlyCollection contentTypeIds) where TContent : IContentBase @@ -484,7 +484,10 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe var dto = new ContentNuDto { - NodeId = cacheNode.Id, Published = published, Data = serialized.StringData, RawData = serialized.ByteData, + NodeId = cacheNode.Id, + Published = published, + Data = serialized.StringData, + RawData = serialized.ByteData, }; return dto; @@ -560,7 +563,10 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe var dto = new ContentNuDto { - NodeId = content.Id, Published = published, Data = serialized.StringData, RawData = serialized.ByteData, + NodeId = content.Id, + Published = published, + Data = serialized.StringData, + RawData = serialized.ByteData, }; return dto; @@ -633,39 +639,30 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe private Sql SqlWhereNodeKey(ISqlContext sqlContext, Guid key) { - ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; - SqlTemplate sqlTemplate = sqlContext.Templates.Get( Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeKey, builder => builder.Where(x => x.UniqueId == SqlTemplate.Arg("key"))); - Sql sql = sqlTemplate.Sql(key); return sql; } private Sql SqlOrderByLevelIdSortOrder(ISqlContext sqlContext) { - ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; - SqlTemplate sqlTemplate = sqlContext.Templates.Get( Constants.SqlTemplates.NuCacheDatabaseDataSource.OrderByLevelIdSortOrder, s => s.OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder)); - Sql sql = sqlTemplate.Sql(); return sql; } private Sql SqlObjectTypeNotTrashed(ISqlContext sqlContext, Guid nodeObjectType) { - ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; - SqlTemplate sqlTemplate = sqlContext.Templates.Get( Constants.SqlTemplates.NuCacheDatabaseDataSource.ObjectTypeNotTrashedFilter, s => s.Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.Trashed == SqlTemplate.Arg("trashed"))); - Sql sql = sqlTemplate.Sql(nodeObjectType, false); return sql; } @@ -681,7 +678,6 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe .From() .InnerJoin().On((left, right) => left.NodeId == right.NodeId) .InnerJoin().On((left, right) => left.NodeId == right.NodeId)); - Sql? sql = sqlTemplate.Sql(); if (joins != null) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index c04e89a769..f9445c5a30 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -58,7 +58,7 @@ "typescript": "5.9.2", "typescript-eslint": "^8.39.1", "typescript-json-schema": "^0.65.1", - "vite": "^7.1.3", + "vite": "^7.1.5", "vite-plugin-static-copy": "^3.1.2", "vite-tsconfig-paths": "^5.1.4", "web-component-analyzer": "^2.0.0" @@ -15451,14 +15451,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -16423,9 +16423,9 @@ } }, "node_modules/vite": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz", - "integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", + "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16434,7 +16434,7 @@ "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", - "tinyglobby": "^0.2.14" + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index bd7be940dd..9f709c5d12 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -256,7 +256,7 @@ "typescript": "5.9.2", "typescript-eslint": "^8.39.1", "typescript-json-schema": "^0.65.1", - "vite": "^7.1.3", + "vite": "^7.1.5", "vite-plugin-static-copy": "^3.1.2", "vite-tsconfig-paths": "^5.1.4", "web-component-analyzer": "^2.0.0" diff --git a/src/Umbraco.Web.UI.Client/src/apps/installer/database/installer-database.element.ts b/src/Umbraco.Web.UI.Client/src/apps/installer/database/installer-database.element.ts index f611a69b91..446de24478 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/installer/database/installer-database.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/installer/database/installer-database.element.ts @@ -268,20 +268,8 @@ export class UmbInstallerDatabaseElement extends UmbLitElement { private _renderCredentials = () => html`

Credentials


- - - - - - + ${this._renderIntegratedAuthentication()} + ${this._renderTrustDatabaseCertificate()} ${ !this.databaseFormData.useIntegratedAuthentication @@ -316,6 +304,34 @@ export class UmbInstallerDatabaseElement extends UmbLitElement { `; + private _renderIntegratedAuthentication() { + if (this._selectedDatabase?.supportsIntegratedAuthentication) { + return html` + + `; + } else { + return null; + } + } + + private _renderTrustDatabaseCertificate() { + if (this._selectedDatabase?.supportsTrustServerCertificate) { + return html` + + `; + } else { + return null; + } + } + private _renderCustom = () => html` Connection string diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 66cc794032..1d8245fdec 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2831,6 +2831,12 @@ export default { resetUrlMessage: 'Are you sure you want to reset this URL?', resetUrlLabel: 'Reset', }, + missingEditor: { + description: + '

Error! This property type is no longer available. Please reach out to your administrator.

', + detailsDescription: + '

This property type is no longer available.
Please contact your administrator so they can either delete this property or restore the property type.

Data:

', + }, uiCulture: { ar: 'العربية', bs: 'Bosanski', @@ -2859,5 +2865,6 @@ export default { uk: 'Українська', zh: '中文', 'zh-tw': '中文(正體,台灣)', + vi: 'Tiếng Việt', }, } as UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts index f94d631f89..7f702cd4a9 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts @@ -2830,4 +2830,10 @@ export default { resetUrlMessage: 'Tem a certeza que quer redefinir este URL?', resetUrlLabel: 'Redefinir', }, + missingEditor: { + description: + '

Erro! Este tipo de propriedade já não se encontra disponível. Por favor, contacte o administrador.

', + detailsDescription: + '

Este tipo de propriedade já não se encontra disponível.
Por favor, contacte o administrador para que ele possa apagar a propriedade ou restaurar o tipo de propriedade.

Dados:

', + }, } as UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/vi.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/vi.ts new file mode 100644 index 0000000000..a5e256b7d1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/vi.ts @@ -0,0 +1,2867 @@ +/** + * Creator Name: The Umbraco community + * Creator Link: https://docs.umbraco.com/umbraco-cms/extending/language-files + * + * Language Alias: vi + * Language Int Name: Vietnamese (VI) + * Language Local Name: Tiếng Việt (VI) + * Language LCID: 1066 + * Language Culture: vi-VN + */ +import type { UmbLocalizationDictionary } from '@umbraco-cms/backoffice/localization-api'; + +export default { + actions: { + assigndomain: 'Ngôn ngữ và Tên miền', + auditTrail: 'Theo dõi Audit', + browse: 'Duyệt', + changeDataType: 'Thay đổi loại dữ liệu', + changeDocType: 'Thay đổi loại tài liệu', + chooseWhereToCopy: 'Chọn nơi để sao chép', + chooseWhereToImport: 'Chọn nơi để nhập', + chooseWhereToMove: 'Chọn nơi để di chuyển', + copy: 'Sao chép', + copyTo: 'Sao chép đến', + create: 'Tạo mới', + createblueprint: 'Tạo tài liệu mẫu', + createGroup: 'Tạo nhóm', + createPackage: 'Tạo gói', + delete: 'Xóa', + disable: 'Tắt', + editContent: 'Chỉnh sửa nội dung', + editSettings: 'Chỉnh sửa cài đặt', + emptyrecyclebin: 'Làm rỗng thùng rác', + enable: 'Bật', + export: 'Xuất khẩu', + exportDocumentType: 'Xuất khẩu loại tài liệu', + folderCreate: 'Tạo thư mục', + folderDelete: 'Xóa', + folderRename: 'Đổi tên', + import: 'Nhập khẩu', + importdocumenttype: 'Nhập khẩu loại tài liệu', + importPackage: 'Nhập khẩu gói', + infiniteEditorChooseWhereToCopy: 'Chọn nơi để sao chép mục đã chọn', + infiniteEditorChooseWhereToMove: 'Chọn nơi để di chuyển mục đã chọn', + liveEdit: 'Chỉnh sửa trong Canvas', + logout: 'Đăng xuất', + move: 'Chuyển đến', + notify: 'Thông báo', + protect: 'Quyền truy cập công khai', + publish: 'Xuất bản', + read: 'Đọc', + readOnly: 'Chỉ đọc', + refreshNode: 'Tải lại mục con', + remove: 'Xóa', + rename: 'Đổi tên', + republish: 'Xuất bản lại toàn bộ trang', + resendInvite: 'Gửi lại lời mời', + restore: 'Khôi phục', + rights: 'Quyền', + rollback: 'Hoàn tác', + sendtopublish: 'Gửi để xuất bản', + sendToTranslate: 'Gửi để dịch', + setGroup: 'Đặt nhóm', + setPermissions: 'Đặt quyền', + sort: 'Sắp xếp mục con', + toInTheTreeStructureBelow: 'đến cấu trúc cây bên dưới', + translate: 'Dịch', + trash: 'Thùng rác', + unlock: 'Mở khóa', + unpublish: 'Hủy xuất bản', + update: 'Cập nhật', + wasCopiedTo: 'đã được sao chép đến', + wasDeleted: 'đã bị xóa', + wasMovedTo: 'đã được di chuyển đến', + viewActionsFor: (name) => (name ? `Xem hành động cho '${name}'` : 'Xem hành động'), + loadMore: 'Tải thêm', + }, + actionCategories: { + content: 'Nội dung', + administration: 'Quản trị', + structure: 'Cấu trúc', + other: 'Khác', + }, + actionDescriptions: { + assignDomain: 'Cho phép truy cập để gán văn hóa và tên miền', + auditTrail: 'Cho phép truy cập để xem nhật ký lịch sử của tài liệu', + browse: 'Cho phép truy cập để xem một tài liệu', + changeDocType: 'Cho phép truy cập để thay đổi loại tài liệu cho một tài liệu', + copy: 'Cho phép truy cập để sao chép một tài liệu', + create: 'Cho phép truy cập để tạo một tài liệu', + delete: 'Cho phép truy cập để xóa một tài liệu', + move: 'Cho phép truy cập để di chuyển một tài liệu', + protect: 'Cho phép truy cập để đặt và thay đổi quyền truy cập cho một tài liệu', + publish: 'Cho phép truy cập để xuất bản một tài liệu', + unpublish: 'Cho phép truy cập để hủy xuất bản một tài liệu', + read: 'Cho phép truy cập để đọc một tài liệu', + rights: 'Cho phép truy cập để thay đổi quyền cho một tài liệu', + rollback: 'Cho phép truy cập để hoàn tác một tài liệu về trạng thái trước đó', + sendtopublish: 'Cho phép truy cập để gửi một tài liệu để phê duyệt trước khi xuất bản', + sendToTranslate: 'Cho phép truy cập để gửi một tài liệu để dịch', + sort: 'Cho phép truy cập để thay đổi thứ tự sắp xếp cho tài liệu', + translate: 'Cho phép truy cập để dịch một tài liệu', + update: 'Cho phép truy cập để lưu một tài liệu', + createblueprint: 'Cho phép truy cập để tạo một tài liệu mẫu', + notify: 'Cho phép truy cập để thiết lập thông báo cho tài liệu', + }, + apps: { + umbContent: 'Nội dung', + umbInfo: 'Thông tin', + }, + assignDomain: { + permissionDenied: 'Quyền truy cập bị từ chối.', + addNew: 'Thêm miền mới', + addCurrent: 'Thêm miền hiện tại', + remove: 'Xóa', + invalidNode: 'Nút không hợp lệ.', + invalidDomain: 'Một hoặc nhiều miền có định dạng không hợp lệ.', + duplicateDomain: 'Miền đã được gán.', + language: 'Ngôn ngữ', + domain: 'Miền', + domainCreated: "Miền mới '%0%' đã được tạo", + domainDeleted: "Miền '%0%' đã bị xóa", + domainExists: "Miền '%0%' đã được gán", + domainUpdated: "Miền '%0%' đã được cập nhật", + orEdit: 'Chỉnh sửa miền hiện tại', + domainHelpWithVariants: + 'Valid domain names are: "example.com", "www.example.com", "example.com:8080", or "https://www.example.com/". Furthermore also one-level paths in domains are supported, e.g. "example.com/en" or "/en".', + inherit: 'Kế thừa', + setLanguage: 'Văn hóa', + setLanguageHelp: + 'Đặt văn hóa cho các nút bên dưới nút hiện tại,
hoặc kế thừa văn hóa từ các nút cha. Sẽ cũng áp dụng
cho nút hiện tại, trừ khi một miền bên dưới cũng áp dụng.', + setDomains: 'Các miền', + }, + buttons: { + clearSelection: 'Xóa lựa chọn', + select: 'Chọn', + choose: 'Chọn', + somethingElse: 'Thực hiện một hành động khác', + bold: 'Đậm', + deindent: 'Hủy bỏ thụt lề đoạn văn', + formFieldInsert: 'Chèn trường biểu mẫu', + graphicHeadline: 'Chèn tiêu đề đồ họa', + htmlEdit: 'Chỉnh sửa Html', + indent: 'Thụt lề đoạn văn', + italic: 'Nghiêng', + justifyCenter: 'Căn giữa', + justifyLeft: 'Căn trái', + justifyRight: 'Căn phải', + linkInsert: 'Chèn liên kết', + linkLocal: 'Chèn liên kết nội bộ (neo)', + listBullet: 'Danh sách gạch đầu dòng', + listNumeric: 'Danh sách số', + macroInsert: 'Chèn macro', + pictureInsert: 'Chèn hình ảnh', + publishAndClose: 'Xuất bản và đóng', + publishDescendants: 'Xuất bản cùng với các phần tử con', + relations: 'Chỉnh sửa quan hệ', + returnToList: 'Quay lại danh sách', + save: 'Lưu', + saveAndClose: 'Lưu và đóng', + saveAndPublish: 'Lưu và xuất bản', + saveToPublish: 'Lưu và gửi phê duyệt', + saveListView: 'Lưu chế độ xem danh sách', + schedulePublish: 'Lập lịch xuất bản', + saveAndPreview: 'Lưu và xem trước', + showPageDisabled: 'Xem trước bị vô hiệu hóa vì không có mẫu nào được chỉ định', + styleChoose: 'Chọn kiểu', + styleShow: 'Hiển thị kiểu', + tableInsert: 'Chèn bảng', + generateModelsAndClose: 'Tạo mô hình và đóng', + saveAndGenerateModels: 'Lưu và tạo mô hình', + undo: 'Hoàn tác', + redo: 'Làm lại', + deleteTag: 'Xóa thẻ', + confirmActionCancel: 'Hủy', + confirmActionConfirm: 'Xác nhận', + morePublishingOptions: 'Thêm tùy chọn xuất bản', + submitChanges: 'Gửi thay đổi', + viewSystemDetails: 'Xem thông tin hệ thống Umbraco CMS và số phiên bản', + }, + auditTrailsMedia: { + delete: 'Đã xóa media', + move: 'Đã di chuyển media', + copy: 'Đã sao chép media', + save: 'Đã lưu media', + }, + auditTrails: { + assigndomain: 'Gán miền: %0%', + atViewingFor: 'Xem cho', + delete: 'Nội dung đã bị xóa', + unpublish: 'Nội dung đã bị hủy xuất bản', + unpublishvariant: 'Nội dung đã bị hủy xuất bản cho các ngôn ngữ: %0%', + publish: 'Nội dung đã được lưu và xuất bản', + publishvariant: 'Nội dung đã được lưu và xuất bản cho các ngôn ngữ: %0%', + save: 'Nội dung đã được lưu', + savevariant: 'Nội dung đã được lưu cho các ngôn ngữ: %0%', + move: 'Nội dung đã được di chuyển', + copy: 'Nội dung đã được sao chép', + rollback: 'Nội dung đã được hoàn tác', + sendtopublish: 'Nội dung đã được gửi để xuất bản', + sendtopublishvariant: 'Nội dung đã được gửi để xuất bản cho các ngôn ngữ: %0%', + sort: 'Sắp xếp các mục con được thực hiện bởi người dùng', + custom: '%0%', + contentversionpreventcleanup: 'Clean up disabled for version: %0%', + contentversionenablecleanup: 'Clean up enabled for version: %0%', + smallAssignDomain: 'Gán miền', + smallCopy: 'Sao chép', + smallPublish: 'Xuất bản', + smallPublishVariant: 'Xuất bản', + smallMove: 'Di chuyển', + smallSave: 'Lưu', + smallSaveVariant: 'Lưu', + smallDelete: 'Xóa', + smallUnpublish: 'Hủy xuất bản', + smallUnpublishVariant: 'Hủy xuất bản', + smallRollBack: 'Hoàn tác', + smallSendToPublish: 'Gửi để xuất bản', + smallSendToPublishVariant: 'Gửi để xuất bản', + smallSort: 'Sắp xếp', + smallCustom: 'Tùy chỉnh', + smallContentVersionPreventCleanup: 'Lưu', + smallContentVersionEnableCleanup: 'Lưu', + historyIncludingVariants: 'Lịch sử (tất cả các biến thể)', + }, + codefile: { + createFolderIllegalChars: 'Tên thư mục không được chứa ký tự bất hợp pháp.', + deleteItemFailed: 'Không thể xóa mục: %0%', + }, + collection: { + noItemsTitle: 'Không có mục nào', + addCollectionConfiguration: 'Thêm bộ sưu tập', + }, + content: { + isPublished: 'Đã xuất bản', + about: 'Về trang này', + alias: 'Bí danh', + alternativeTextHelp: '(how would you describe the picture over the phone)', + alternativeUrls: 'Liên kết thay thế', + clickToEdit: 'Nhấp để chỉnh sửa mục này', + createBy: 'Được tạo bởi', + createByDesc: 'Tác giả gốc', + updatedBy: 'Được cập nhật bởi', + createDate: 'Ngày tạo', + createDateDesc: 'Ngày/giờ tài liệu này được tạo', + documentType: 'Loại Tài liệu', + editing: 'Đang chỉnh sửa', + expireDate: 'Xóa vào', + itemChanged: 'Mục này đã được thay đổi sau khi xuất bản', + itemNotPublished: 'Mục này chưa được xuất bản', + lastPublished: 'Lần xuất bản cuối', + noItemsToShow: 'Không có mục nào để hiển thị', + listViewNoItems: 'Không có mục nào để hiển thị trong danh sách.', + listViewNoContent: 'Không có nội dung nào được thêm vào', + listViewNoMembers: 'Không có thành viên nào được thêm vào', + mediatype: 'Loại phương tiện', + mediaLinks: 'Liên kết đến mục phương tiện', + membergroup: 'Nhóm thành viên', + memberrole: 'Vai trò', + membertype: 'Loại Thành viên', + noChanges: 'Không có thay đổi nào được thực hiện', + noDate: 'Không có ngày nào được chọn', + nodeName: 'Tiêu đề trang', + noMediaLink: 'Phương tiện này chưa có liên kết', + noProperties: 'Không có nội dung nào có thể được thêm vào mục này', + otherElements: 'Thuộc tính', + parentNotPublished: + "Tài liệu này đã được xuất bản nhưng không thể nhìn thấy vì tài liệu cha '%0%' chưa được xuất bản", + parentCultureNotPublished: + "Văn hóa này đã được xuất bản nhưng không thể nhìn thấy vì nó chưa được xuất bản trên tài liệu cha '%0%'", + parentNotPublishedAnomaly: 'Tài liệu này đã được xuất bản nhưng không có trong bộ nhớ cache', + getUrlException: 'Không thể lấy URL', + routeError: 'Tài liệu này đã được xuất bản nhưng URL của nó sẽ va chạm với nội dung %0%', + routeErrorCannotRoute: 'Tài liệu này đã được xuất bản nhưng không thể truy cập bằng URL', + publish: 'Xuất bản', + published: 'Đã xuất bản', + publishedPendingChanges: 'Đã xuất bản (đang chờ thay đổi)', + publishStatus: 'Trạng thái xuất bản', + publishDescendantsHelp: + 'Xuất bản %0% và tất cả các mục bên dưới và do đó làm cho nội dung của chúng có sẵn công khai.', + publishDescendantsWithVariantsHelp: + 'Xuất bản các biến thể và các biến thể cùng loại bên dưới và do đó làm cho nội dung của chúng có sẵn công khai.', + noVariantsToProcess: 'Không có biến thể nào có sẵn', + releaseDate: 'Xuất bản vào', + unpublishDate: 'Hủy xuất bản vào', + removeDate: 'Xóa ngày', + setDate: 'Đặt ngày', + sortDone: 'Thứ tự sắp xếp đã được cập nhật', + sortHelp: + 'Để sắp xếp các nút, chỉ cần kéo các nút hoặc nhấp vào một trong các tiêu đề cột. Bạn có thể chọn nhiều nút bằng cách giữ phím "shift" hoặc "control" trong khi chọn', + statistics: 'Thống kê', + titleOptional: 'Tiêu đề (tùy chọn)', + altTextOptional: 'Văn bản thay thế (tùy chọn)', + captionTextOptional: 'Chú thích (tùy chọn)', + type: 'Loại', + unpublish: 'Hủy xuất bản', + unpublished: 'Chưa xuất bản', + notCreated: 'Chưa được tạo', + updateDate: 'Chỉnh sửa lần cuối', + updateDateDesc: 'Ngày/giờ tài liệu này được chỉnh sửa', + uploadClear: 'Xóa tệp', + uploadClearImageContext: 'Nhấp vào đây để xóa hình ảnh khỏi mục phương tiện', + uploadClearFileContext: 'Nhấp vào đây để xóa tệp khỏi mục phương tiện', + urls: 'Liên kết đến tài liệu', + memberof: 'Thành viên của nhóm', + notmemberof: 'Không phải là thành viên của nhóm', + childItems: 'Các mục con', + target: 'Mục tiêu', + scheduledPendingChanges: 'Lịch trình này có các thay đổi sẽ có hiệu lực khi bạn nhấp vào "%0%".', + scheduledPublishServerTime: 'Điều này tương đương với thời gian sau trên máy chủ:', + scheduledPublishDocumentation: + 'Điều này có nghĩa là gì?', + nestedContentDeleteItem: 'Bạn có chắc chắn muốn xóa mục này không?', + nestedContentDeleteAllItems: 'Bạn có chắc chắn muốn xóa tất cả các mục không?', + nestedContentEditorNotSupported: 'Thuộc tính %0% sử dụng trình chỉnh sửa %1% không được Nested Content hỗ trợ.', + nestedContentNoContentTypes: 'Không có loại nội dung nào được cấu hình cho thuộc tính này.', + nestedContentAddElementType: 'Thêm loại phần tử', + nestedContentSelectElementTypeModalTitle: 'Chọn loại phần tử', + nestedContentGroupHelpText: + 'Chọn nhóm mà thuộc tính của nó sẽ được hiển thị. Nếu để trống, nhóm đầu tiên trên loại phần tử sẽ được sử dụng.', + nestedContentTemplateHelpTextPart1: 'Nhập một biểu thức angular để đánh giá từng mục theo tên của nó. Sử dụng', + nestedContentTemplateHelpTextPart2: 'để hiển thị chỉ mục của mục', + nestedContentNoGroups: + 'Loại phần tử đã chọn không chứa bất kỳ nhóm nào được hỗ trợ (tab không được trình chỉnh sửa này hỗ trợ, hãy thay đổi chúng thành nhóm hoặc sử dụng trình chỉnh sửa danh sách khối).', + addTextBox: 'Thêm một ô văn bản khác', + removeTextBox: 'Xóa ô văn bản này', + contentRoot: 'Gốc nội dung', + includeUnpublished: 'Bao gồm các mục chưa xuất bản.', + isSensitiveValue: + 'Giá trị này bị ẩn. Nếu bạn cần quyền truy cập để xem giá trị này, vui lòng liên hệ với quản trị viên trang web của bạn.', + isSensitiveValue_short: 'Giá trị này bị ẩn.', + languagesToPublish: 'Bạn muốn xuất bản ngôn ngữ nào?', + languagesToSendForApproval: 'Bạn muốn gửi ngôn ngữ nào để phê duyệt?', + languagesToSchedule: 'Bạn muốn lên lịch ngôn ngữ nào?', + languagesToUnpublish: + 'Chọn ngôn ngữ để hủy xuất bản. Hủy xuất bản một ngôn ngữ bắt buộc sẽ hủy xuất bản tất cả các ngôn ngữ.', + variantsWillBeSaved: 'Tất cả các biến thể mới sẽ được lưu.', + variantsToPublish: 'Bạn muốn xuất bản biến thể nào?', + variantsToSave: 'Chọn biến thể nào sẽ được lưu.', + publishRequiresVariants: 'Các biến thể sau đây là bắt buộc để xuất bản:', + notReadyToPublish: 'Chúng tôi chưa sẵn sàng để xuất bản', + readyToPublish: 'Sẵn sàng để xuất bản?', + readyToSave: 'Sẵn sàng để lưu?', + resetFocalPoint: 'Đặt lại điểm tiêu cự', + sendForApproval: 'Gửi để phê duyệt', + schedulePublishHelp: 'Chọn ngày và giờ để xuất bản và/hoặc hủy xuất bản mục nội dung.', + createEmpty: 'Tạo mới', + createFromClipboard: 'Dán từ clipboard', + nodeIsInTrash: 'Mục này đang ở trong Thùng rác', + variantSaveNotAllowed: 'Lưu không được phép', + variantPublishNotAllowed: 'Xuất bản không được phép', + variantSendForApprovalNotAllowed: 'Gửi để phê duyệt không được phép', + variantScheduleNotAllowed: 'Lên lịch không được phép', + variantUnpublishNotAllowed: 'Hủy xuất bản không được phép', + selectAllVariants: 'Chọn tất cả các biến thể', + saveModalTitle: 'Lưu', + saveAndPublishModalTitle: 'Lưu và xuất bản', + publishModalTitle: 'Xuất bản', + }, + blueprints: { + createBlueprintFrom: "Tạo một tài liệu Blueprint mới từ '%0%'", + createBlueprintItemUnder: "Tạo một mục mới dưới '%0%'", + createBlueprintFolderUnder: "Tạo một thư mục mới dưới '%0%'", + blankBlueprint: 'Trống', + selectBlueprint: 'Chọn một tài liệu Blueprint', + createdBlueprintHeading: 'Tài liệu Blueprint đã được tạo', + createdBlueprintMessage: "Một tài liệu Blueprint đã được tạo từ '%0%'", + duplicateBlueprintMessage: 'Một tài liệu Blueprint khác với cùng tên đã tồn tại', + blueprintDescription: + 'Tài liệu Blueprint là nội dung được định nghĩa trước mà biên tập viên có thể chọn để sử dụng làm cơ sở cho việc tạo nội dung mới', + }, + entityDetail: { + notFoundTitle: (entityType: string) => { + const entityName = entityType ?? 'Mục'; + return `${entityName} không tìm thấy`; + }, + notFoundDescription: (entityType: string) => { + const entityName = entityType ?? 'Mục'; + return `Mục ${entityName} yêu cầu không thể tìm thấy. Vui lòng kiểm tra lại URL và thử lại.`; + }, + forbiddenTitle: (entityType: string) => { + const entityName = entityType ?? 'Mục'; + return `Truy cập bị từ chối đối với ${entityName} này`; + }, + forbiddenDescription: (entityType: string) => { + const entityName = entityType ?? 'Mục'; + return `Bạn không có quyền truy cập vào ${entityName} này. Vui lòng liên hệ với quản trị viên của bạn để được hỗ trợ.`; + }, + }, + media: { + clickToUpload: 'Nhấp để tải lên', + orClickHereToUpload: 'hoặc nhấp vào đây để chọn tệp', + disallowedFileType: 'Không thể tải lên tệp này, nó không có loại tệp được phê duyệt', + disallowedMediaType: "Không thể tải lên tệp này, loại phương tiện có bí danh '%0%' không được phép ở đây", + invalidFileName: 'Không thể tải lên tệp này, nó không có tên tệp hợp lệ', + invalidFileSize: 'Không thể tải lên tệp này, nó quá lớn', + maxFileSize: 'Kích thước tệp tối đa là', + mediaRoot: 'Thư mục gốc', + createFolderFailed: 'Không thể tạo thư mục dưới id cha %0%', + renameFolderFailed: 'Không thể đổi tên thư mục với id %0%', + dragAndDropYourFilesIntoTheArea: 'Kéo và thả tệp của bạn vào khu vực này', + fileSecurityValidationFailure: 'Một hoặc nhiều xác thực bảo mật tệp đã thất bại', + moveToSameFolderFailed: 'Thư mục cha và thư mục đích không thể giống nhau', + uploadNotAllowed: 'Tải lên không được phép ở vị trí này.', + }, + member: { + '2fa': 'Xác thực hai yếu tố', + allMembers: 'Tất cả thành viên', + createNewMember: 'Tạo thành viên mới', + duplicateMemberLogin: 'Một thành viên với đăng nhập này đã tồn tại', + kind: 'Loại', + memberGroupNoProperties: 'Nhóm thành viên không có thuộc tính bổ sung để chỉnh sửa.', + memberHasGroup: "Thành viên đã ở trong nhóm '%0%'", + memberHasPassword: 'Thành viên đã có mật khẩu được đặt', + memberKindDefault: 'Thành viên', + memberKindApi: 'Thành viên API', + memberLockoutNotEnabled: 'Tính năng khóa tài khoản không được bật cho thành viên này', + memberNotInGroup: "Thành viên không ở trong nhóm '%0%'", + }, + contentType: { + copyFailed: 'Không thể sao chép loại nội dung', + moveFailed: 'Không thể di chuyển loại nội dung', + contentTypes: 'Các loại nội dung', + }, + mediaType: { + copyFailed: 'Không thể sao chép loại phương tiện', + moveFailed: 'Không thể di chuyển loại phương tiện', + autoPickMediaType: 'Tự động chọn', + }, + memberType: { + copyFailed: 'Không thể sao chép loại thành viên', + }, + create: { + chooseNode: 'Bạn muốn tạo %0% ở đâu', + createUnder: 'Thêm mục mới trong', + createContentBlueprint: 'Chọn loại tài liệu bạn muốn tạo bản mẫu tài liệu cho', + enterFolderName: 'Nhập tên thư mục', + updateData: 'Chọn loại và tiêu đề', + noDocumentTypes: + 'Không có loại tài liệu nào được phép để tạo nội dung ở đây. Bạn phải kích hoạt chúng trong Document Types trong phần Settings, bằng cách chỉnh sửa Allowed child node types dưới Permissions.', + noDocumentTypesAtRoot: + 'Không có loại tài liệu nào có sẵn để tạo nội dung ở đây. Bạn phải tạo chúng trong Document Types trong phần Settings.', + noDocumentTypesWithNoSettingsAccess: + 'Trang được chọn trong cây nội dung không cho phép tạo bất kỳ trang nào bên dưới nó.', + noDocumentTypesEditPermissions: 'Chỉnh sửa quyền cho loại tài liệu này', + noDocumentTypesCreateNew: 'Tạo một loại tài liệu mới', + noDocumentTypesAllowedAtRoot: + 'Không có loại tài liệu nào được phép có sẵn để tạo nội dung ở đây. Bạn phải kích hoạt chúng trong Document Types trong phần Settings, bằng cách thay đổi tùy chọn Allow as root dưới Permissions.', + noMediaTypes: + 'Không có loại phương tiện nào được phép có sẵn để tạo phương tiện ở đây. Bạn phải kích hoạt chúng trong Media Types trong phần Settings, bằng cách chỉnh sửa Allowed child node types dưới Permissions.', + noMediaTypesWithNoSettingsAccess: + 'Phương tiện được chọn trong cây không cho phép tạo bất kỳ phương tiện nào bên dưới nó.', + noMediaTypesEditPermissions: 'Chỉnh sửa quyền cho loại phương tiện này', + documentTypeWithoutTemplate: 'Loại tài liệu không có mẫu', + documentTypeWithTemplate: 'Loại tài liệu có mẫu', + documentTypeWithTemplateDescription: + 'Định nghĩa dữ liệu cho một trang nội dung có thể được tạo bởi các biên tập viên trong cây nội dung và có thể truy cập trực tiếp qua URL.', + documentType: 'Loại tài liệu', + documentTypeDescription: + 'Định nghĩa dữ liệu cho một thành phần nội dung có thể được tạo bởi các biên tập viên trong cây nội dung và có thể được chọn trên các trang khác nhưng không có URL trực tiếp.', + elementType: 'Loại phần tử', + elementTypeDescription: + "Xác định lược đồ cho một tập hợp thuộc tính lặp lại, ví dụ như trong trình chỉnh sửa thuộc tính 'Block List' hoặc 'Block Grid'.", + composition: 'Composition', + compositionDescription: + "Xác định một tập hợp thuộc tính có thể tái sử dụng, được đưa vào định nghĩa của nhiều Loại tài liệu khác nhau. Ví dụ: một tập hợp 'Cài đặt trang chung'.", + folder: 'Thư mục', + folderDescription: 'Được sử dụng để tổ chức các mục và thư mục khác. Giữ cho các mục có cấu trúc và dễ truy cập.', + newFolder: 'Thư mục mới', + newDataType: 'Loại dữ liệu mới', + newDataTypeDescription: 'Được sử dụng để định nghĩa cấu hình cho một Loại thuộc tính trên một Loại nội dung.', + newJavascriptFile: 'Tệp JavaScript mới', + newEmptyPartialView: 'Tạo một phần xem tạm thời mới', + newPartialViewMacro: 'Tạo một macro phần xem tạm thời mới', + newPartialViewFromSnippet: 'Tạo một phần xem tạm thời mới từ đoạn mã', + newPartialViewMacroFromSnippet: 'Tạo một macro phần xem tạm thời mới từ đoạn mã', + newPartialViewMacroNoMacro: 'Macro phần xem tạm thời mới (không có macro)', + newStyleSheetFile: 'Tạo một tệp Stylesheet mới', + }, + dashboard: { + browser: 'Duyệt trang web của bạn', + dontShowAgain: '- Ẩn', + nothinghappens: 'Nếu Umbraco không mở, bạn có thể cần cho phép cửa sổ bật lên từ trang này', + openinnew: 'đã mở trong một cửa sổ mới', + restart: 'Khởi động lại', + visit: 'Thăm', + welcome: 'Chào mừng', + }, + prompt: { + stay: 'Ở lại', + discardChanges: 'Huỷ thay đổi', + unsavedChanges: 'Hủy thay đổi chưa lưu', + unsavedChangesWarning: 'Bạn có chắc chắn muốn rời khỏi trang này? Một số thay đổi của bạn chưa được lưu.', + confirmListViewPublish: 'Xuất bản sẽ làm cho các mục đã chọn có sẵn công khai.', + confirmListViewUnpublish: + 'Ngừng xuất bản sẽ làm cho các mục đã chọn và tất cả các mục con của chúng không còn khả dụng công khai.', + confirmPublish: 'Xuất bản sẽ làm cho nội dung này và tất cả các mục con đã xuất bản của nó có sẵn công khai.', + confirmUnpublish: 'Ngừng xuất bản sẽ làm cho nội dung này không còn khả dụng công khai.', + doctypeChangeWarning: 'Bạn có thay đổi chưa lưu. Thay đổi Loại tài liệu sẽ hủy bỏ các thay đổi.', + }, + bulk: { + done: 'Hoàn tất', + deletedItem: 'Đã xóa %0% mục', + deletedItems: 'Đã xóa %0% mục', + deletedItemOfItem: 'Đã xóa %0% trong số %1% mục', + deletedItemOfItems: 'Đã xóa %0% trong số %1% mục', + publishedItem: 'Đã xuất bản %0% mục', + publishedItems: 'Đã xuất bản %0% mục', + publishedItemOfItem: 'Đã xuất bản %0% trong số %1% mục', + publishedItemOfItems: 'Đã xuất bản %0% trong số %1% mục', + unpublishedItem: 'Chưa xuất bản %0% mục', + unpublishedItems: 'Chưa xuất bản %0% mục', + unpublishedItemOfItem: 'Chưa xuất bản %0% trong số %1% mục', + unpublishedItemOfItems: 'Chưa xuất bản %0% trong số %1% mục', + movedItem: 'Đã di chuyển %0% mục', + movedItems: 'Đã di chuyển %0% mục', + movedItemOfItem: 'Đã di chuyển %0% trong số %1% mục', + movedItemOfItems: 'Đã di chuyển %0% trong số %1% mục', + copiedItem: 'Đã sao chép %0% mục', + copiedItems: 'Đã sao chép %0% mục', + copiedItemOfItem: 'Đã sao chép %0% trong số %1% mục', + copiedItemOfItems: 'Đã sao chép %0% trong số %1% mục', + }, + defaultdialogs: { + nodeNameLinkPicker: 'Tiêu đề', + urlLinkPicker: 'Link', + anchorLinkPicker: 'Anchor or querystring', + anchorInsert: 'Tên', + closeThisWindow: 'Đóng cửa sổ này', + confirmdelete: (name: string) => `Bạn có chắc chắn muốn xóa${name ? ` ${name}` : ''}?`, + confirmdeleteNumberOfItems: 'Bạn có chắc chắn muốn xóa %0% trong số %1% mục?', + confirmdisable: 'Bạn có chắc chắn muốn vô hiệu hóa', + confirmremove: 'Bạn có chắc chắn muốn xóa', + confirmremoveusageof: 'Bạn có chắc chắn muốn xóa việc sử dụng %0% không?', + confirmlogout: 'Bạn có chắc chắn muốn đăng xuất?', + confirmSure: 'Bạn có chắc chắn?', + confirmTrash: (name: string) => `Bạn có chắc chắn muốn di chuyển ${name} vào Thùng rác?`, + confirmBulkTrash: (total: number) => + `Bạn có chắc chắn muốn di chuyển ${total} ${total === 1 ? 'mục' : 'các mục'} vào Thùng rác?`, + confirmBulkDelete: (total: number) => + `Bạn có chắc chắn muốn xóa ${total} ${total === 1 ? 'mục' : 'các mục'}?`, + cut: 'Cắt', + editDictionary: 'Chỉnh sửa mục từ điển', + editLanguage: 'Chỉnh sửa ngôn ngữ', + editSelectedMedia: 'Chỉnh sửa phương tiện đã chọn', + editWebhook: 'Chỉnh sửa webhook', + insertAnchor: 'Chèn liên kết nội bộ', + insertCharacter: 'Chèn ký tự', + insertgraphicheadline: 'Chèn tiêu đề đồ họa', + insertimage: 'Chèn hình ảnh', + insertlink: 'Chèn liên kết', + insertMacro: 'Nhấp để thêm Macro', + inserttable: 'Chèn bảng', + languagedeletewarning: 'Điều này sẽ xóa ngôn ngữ và tất cả nội dung liên quan đến ngôn ngữ đó', + languageChangeWarning: + 'Thay đổi cài đặt văn hóa cho một ngôn ngữ có thể là một thao tác tốn kém và sẽ dẫn đến việc xây dựng lại bộ nhớ đệm nội dung và các chỉ mục.', + lastEdited: 'Lần chỉnh sửa cuối', + link: 'Liên kết', + linkinternal: 'Liên kết nội bộ', + linklocaltip: 'Khi sử dụng liên kết cục bộ, hãy chèn "#" ở phía trước liên kết', + linknewwindow: 'Mở trong cửa sổ mới?', + macroDoesNotHaveProperties: 'Macro này không chứa bất kỳ thuộc tính nào bạn có thể chỉnh sửa', + paste: 'Dán', + permissionsEdit: 'Chỉnh sửa quyền cho', + permissionsSet: 'Đặt quyền cho', + permissionsSetForGroup: 'Đặt quyền cho %0% cho nhóm người dùng %1%', + permissionsHelp: 'Chọn nhóm người dùng mà bạn muốn đặt quyền cho', + recycleBinDeleting: + 'Các mục trong thùng rác hiện đang được xóa. Vui lòng không đóng cửa sổ này trong khi thao tác này đang diễn ra', + recycleBinIsEmpty: 'Thùng rác hiện đang trống', + recycleBinWarning: 'Khi các mục bị xóa khỏi thùng rác, chúng sẽ biến mất vĩnh viễn', + regexSearchError: + "regexlib.com hiện đang gặp một số sự cố mà chúng tôi không thể kiểm soát. Chúng tôi rất tiếc về sự bất tiện này.", + regexSearchHelp: + "Tìm kiếm một biểu thức chính quy để thêm xác thực vào một trường biểu mẫu. Ví dụ: 'email, 'zip-code', 'URL'.", + removeMacro: 'Xóa Macro', + requiredField: 'Trường bắt buộc', + sitereindexed: 'Trang web đã được lập chỉ mục lại', + siterepublished: + 'Bộ nhớ đệm của trang web đã được làm mới. Tất cả nội dung đã xuất bản hiện đã được cập nhật. Trong khi tất cả nội dung chưa xuất bản vẫn chưa được xuất bản', + siterepublishHelp: + 'Bộ nhớ đệm của trang web sẽ được làm mới. Tất cả nội dung đã xuất bản sẽ được cập nhật, trong khi nội dung chưa xuất bản sẽ vẫn chưa được xuất bản.', + tableColumns: 'Số lượng cột', + tableRows: 'Số lượng hàng', + thumbnailimageclickfororiginal: 'Nhấp vào hình để xem kích thước đầy đủ', + treepicker: 'Chọn mục', + viewCacheItem: 'Xem mục bộ nhớ cache', + relateToOriginalLabel: 'Liên kết với bản gốc', + includeDescendants: 'Bao gồm các phần tử con', + theFriendliestCommunity: 'Cộng đồng thân thiện nhất', + linkToPage: 'Liên kết đến tài liệu', + openInNewWindow: 'Mở liên kết trong cửa sổ hoặc tab mới', + linkToMedia: 'Liên kết đến phương tiện', + selectContentStartNode: 'Chọn nút bắt đầu nội dung', + selectEvent: 'Chọn sự kiện', + selectMedia: 'Chọn phương tiện', + chooseMedia: 'Chọn phương tiện', + chooseMediaStartNode: 'Chọn nút bắt đầu phương tiện', + selectMediaType: 'Chọn loại phương tiện', + selectIcon: 'Chọn biểu tượng', + selectItem: 'Chọn mục', + selectLink: 'Cấu hình liên kết', + addLink: 'Thêm liên kết', + updateLink: 'Cập nhật liên kết', + selectMacro: 'Chọn macro', + selectContent: 'Chọn nội dung', + selectContentType: 'Chọn loại nội dung', + selectMediaStartNode: 'Chọn nút bắt đầu media', + selectMember: 'Chọn thành viên', + selectMembers: 'Chọn các thành viên', + selectMemberGroup: 'Chọn nhóm thành viên', + chooseMemberGroup: 'Chọn nhóm thành viên', + selectMemberType: 'Chọn loại thành viên', + selectNode: 'Chọn nút', + selectLanguages: 'Chọn ngôn ngữ', + selectSections: 'Chọn phần', + selectUser: 'Chọn người dùng', + selectUsers: 'Chọn người dùng', + chooseUsers: 'Chọn người dùng', + noIconsFound: 'Không tìm thấy biểu tượng', + noMacroParams: 'Không có tham số nào cho macro này', + noMacros: 'Không có macro nào có sẵn để chèn', + externalLoginProviders: 'Đăng nhập bên ngoài', + exceptionDetail: 'Chi tiết ngoại lệ', + stacktrace: 'Stacktrace', + innerException: 'Inner Exception', + linkYour: 'Liên kết tài khoản {0} của bạn', + linkYourConfirm: + 'Bạn sắp liên kết tài khoản Umbraco và {0} của mình và bạn sẽ được chuyển hướng đến {0} để xác nhận.', + unLinkYour: 'Hủy liên kết tài khoản {0} của bạn', + unLinkYourConfirm: 'Bạn sắp hủy liên kết tài khoản Umbraco và {0} của mình và bạn sẽ bị đăng xuất.', + linkedToService: 'Tài khoản của bạn đã được liên kết với dịch vụ này', + selectEditor: 'Chọn trình chỉnh sửa', + selectEditorConfiguration: 'Chọn cấu hình', + selectSnippet: 'Chọn đoạn mã', + variantdeletewarning: + 'Điều này sẽ xóa node và tất cả các ngôn ngữ của nó. Nếu bạn chỉ muốn xóa một ngôn ngữ, hãy hủy xuất bản node trong ngôn ngữ đó thay vì xóa.', + propertyuserpickerremovewarning: 'Điều này sẽ xóa người dùng %0%.', + userremovewarning: 'Điều này sẽ xóa người dùng %0% khỏi nhóm %1%', + yesRemove: 'Có, xóa', + deleteLayout: 'Bạn đang xóa bố cục', + deletingALayout: + 'Việc sửa đổi bố cục sẽ dẫn đến mất dữ liệu cho bất kỳ nội dung hiện có nào dựa trên cấu hình này.', + seeErrorAction: 'Xem lỗi', + seeErrorDialogHeadline: 'Chi tiết lỗi', + }, + dictionary: { + importDictionaryItemHelp: + 'Để nhập một mục từ điển, hãy tìm tệp ".udt" trên máy tính của bạn bằng cách nhấp vào nút "Thêm" (bạn sẽ được yêu cầu xác nhận trên màn hình tiếp theo).', + itemDoesNotExists: 'Mục từ điển không tồn tại.', + parentDoesNotExists: 'Mục cha không tồn tại.', + noItems: 'Không có mục từ điển nào.', + noItemsInFile: 'Không có mục từ điển nào trong tệp này.', + noItemsFound: 'Không tìm thấy mục từ điển nào.', + createNew: 'Tạo mục từ điển mới', + pickFile: 'Chọn tệp', + pickFileRequired: 'Vui lòng chọn tệp ".udt"', + }, + dictionaryItem: { + description: "Chỉnh sửa các phiên bản ngôn ngữ khác nhau cho mục từ điển '%0%' bên dưới", + displayName: 'Tên văn hóa', + changeKeyError: "Khóa '%0%' đã tồn tại.", + overviewTitle: 'Tổng quan từ điển', + }, + examineManagement: { + configuredSearchers: 'Các tìm kiếm đã cấu hình', + configuredSearchersDescription: + 'Hiển thị các thuộc tính và công cụ cho bất kỳ tìm kiếm nào đã cấu hình (ví dụ: như một tìm kiếm đa chỉ mục)', + fieldValues: 'Giá trị trường', + healthStatus: 'Trạng thái sức khỏe', + healthStatusDescription: 'Trạng thái sức khỏe của chỉ mục và nếu nó có thể được đọc', + indexers: 'Bộ lập chỉ mục', + indexInfo: 'Thông tin chỉ mục', + contentInIndex: 'Nội dung trong chỉ mục', + indexInfoDescription: 'Liệt kê các thuộc tính của chỉ mục', + manageIndexes: 'Quản lý các chỉ mục Examine', + manageIndexesDescription: + 'Cho phép bạn xem chi tiết của từng chỉ mục và cung cấp một số công cụ để quản lý các chỉ mục', + rebuildIndex: 'Xây dựng lại chỉ mục', + rebuildIndexWarning: + 'Điều này sẽ khiến chỉ mục được xây dựng lại.
Tùy thuộc vào lượng nội dung trên trang web của bạn, quá trình này có thể mất một thời gian.
Không khuyến nghị xây dựng lại chỉ mục trong thời gian lưu lượng truy cập cao hoặc khi các biên tập viên đang chỉnh sửa nội dung.', + searchers: 'Bộ tìm kiếm', + searchDescription: 'Tìm kiếm trong chỉ mục và xem kết quả', + tools: 'Công cụ', + toolsDescription: 'Công cụ quản lý chỉ mục', + fields: 'trường', + indexCannotRead: 'Không thể đọc chỉ mục và cần được xây dựng lại', + processIsTakingLonger: + 'Quá trình mất nhiều thời gian hơn dự kiến, hãy kiểm tra nhật ký Umbraco để xem có lỗi nào trong quá trình này không', + indexCannotRebuild: 'Chỉ mục này không thể được xây dựng lại vì không có IIndexPopulator được gán', + iIndexPopulator: 'IIndexPopulator', + noResults: 'Không tìm thấy kết quả nào', + searchResultsFound: 'Hiển thị %0% - %1% của %2% kết quả - Trang %3% trên %4%', + corruptStatus: 'Phát hiện chỉ mục có khả năng bị hỏng', + corruptErrorDescription: 'Lỗi nhận được khi đánh giá chỉ mục:', + }, + placeholders: { + username: 'Nhập tên người dùng...', + password: 'Nhập mật khẩu...', + confirmPassword: 'Xác nhận mật khẩu...', + nameentity: 'Đặt tên cho %0%...', + entername: 'Nhập tên...', + enteremail: 'Nhập email...', + enterusername: 'Nhập tên người dùng...', + enterdate: 'Chọn ngày...', + label: 'Nhãn...', + enterDescription: 'Nhập mô tả...', + search: 'Nhập để tìm kiếm...', + filter: 'Nhập để lọc...', + enterTags: 'Nhập để thêm thẻ (nhấn enter sau mỗi thẻ)...', + email: 'Nhập email của bạn', + enterMessage: 'Nhập tin nhắn...', + usernameHint: 'Tên người dùng thường là email của bạn', + anchor: 'Nhập anchor hoặc querystring, #giá_trị hoặc ?key=giá_trị', + enterAlias: 'Nhập bí danh...', + generatingAlias: 'Đang tạo bí danh...', + a11yCreateItem: 'Tạo mục', + a11yEdit: 'Chỉnh sửa', + a11yName: 'Tên', + rteParagraph: 'Viết điều gì đó tuyệt vời...', + rteHeading: 'Tiêu đề là gì?', + enterUrl: 'Nhập URL...', + }, + editcontenttype: { + createListView: 'Tạo chế độ xem danh sách tùy chỉnh', + removeListView: 'Xóa chế độ xem danh sách tùy chỉnh', + aliasAlreadyExists: 'Một Kiểu Nội dung, Kiểu Media hoặc Kiểu Thành viên với bí danh này đã tồn tại', + }, + renamecontainer: { + renamed: 'Đã đổi tên', + enterNewFolderName: 'Nhập tên thư mục mới ở đây', + folderWasRenamed: '%0% đã được đổi tên thành %1%', + }, + editdatatype: { + canChangePropertyEditorHelp: + 'Việc thay đổi trình chỉnh sửa thuộc tính trên một kiểu dữ liệu đã có giá trị lưu trữ bị vô hiệu hóa. Để cho phép điều này, bạn có thể thay đổi cài đặt Umbraco:CMS:DataTypes:CanBeChanged trong appsettings.json.', + addPrevalue: 'Thêm giá trị trước', + dataBaseDatatype: 'Kiểu dữ liệu cơ sở dữ liệu', + guid: 'GUID trình chỉnh sửa thuộc tính', + renderControl: 'Trình chỉnh sửa thuộc tính', + rteButtons: 'Các nút', + rteEnableAdvancedSettings: 'Bật cài đặt nâng cao cho', + rteEnableContextMenu: 'Bật menu ngữ cảnh', + rteMaximumDefaultImgSize: 'Kích thước mặc định tối đa của hình ảnh được chèn', + rteRelatedStylesheets: 'Các stylesheet liên quan', + rteShowLabel: 'Hiển thị nhãn', + rteWidthAndHeight: 'Chiều rộng và chiều cao', + selectFolder: 'Chọn thư mục để di chuyển', + inTheTree: 'đến trong cấu trúc cây bên dưới', + wasMoved: 'đã được di chuyển dưới', + hasReferencesDeleteConsequence: + 'Xóa %0% sẽ xóa các thuộc tính và dữ liệu của chúng khỏi các mục sau', + acceptDeleteConsequence: 'Tôi hiểu hành động này sẽ xóa các thuộc tính và dữ liệu dựa trên Kiểu dữ liệu này', + noConfiguration: 'Không có cấu hình cho trình chỉnh sửa thuộc tính này.', + }, + errorHandling: { + errorButDataWasSaved: + 'Dữ liệu của bạn đã được lưu, nhưng trước khi bạn có thể xuất bản trang này, có một số lỗi bạn cần sửa trước:', + errorChangingProviderPassword: + 'Nhà cung cấp thành viên hiện tại không hỗ trợ thay đổi mật khẩu (EnablePasswordRetrieval cần được đặt là true)', + errorExistsWithoutTab: '%0% đã tồn tại', + errorHeader: 'Đã xảy ra lỗi:', + errorHeaderWithoutTab: 'Đã xảy ra lỗi:', + errorInPasswordFormat: 'Mật khẩu phải có tối thiểu %0% ký tự và chứa ít nhất %1% ký tự không phải chữ cái hoặc số', + errorIntegerWithoutTab: '%0% phải là một số nguyên', + errorMandatory: 'Trường %0% trong tab %1% là bắt buộc', + errorMandatoryWithoutTab: '%0% là trường bắt buộc', + errorRegExp: '%0% tại %1% không đúng định dạng', + errorRegExpWithoutTab: '%0% không đúng định dạng', + }, + errors: { + defaultError: 'Đã xảy ra lỗi không xác định', + concurrencyError: 'Lỗi đồng thời tối ưu, đối tượng đã bị sửa đổi', + receivedErrorFromServer: 'Nhận được lỗi từ máy chủ', + dissallowedMediaType: 'Loại tệp được chỉ định đã bị quản trị viên cấm', + codemirroriewarning: + 'CHÚ Ý! Mặc dù CodeMirror được bật theo cấu hình, nhưng nó bị tắt trên Internet Explorer vì không đủ ổn định.', + contentTypeAliasAndNameNotNull: 'Vui lòng điền cả alias và tên cho loại thuộc tính mới!', + filePermissionsError: 'Có vấn đề với quyền đọc/ghi trên một tệp hoặc thư mục cụ thể', + macroErrorLoadingPartialView: 'Lỗi khi tải script Partial View (tệp: %0%)', + missingTitle: 'Vui lòng nhập tiêu đề', + missingType: 'Vui lòng chọn một loại', + pictureResizeBiggerThanOrg: 'Bạn sắp làm hình lớn hơn kích thước gốc. Bạn có chắc muốn tiếp tục?', + startNodeDoesNotExists: 'Startnode đã bị xóa, vui lòng liên hệ quản trị viên', + stylesMustMarkBeforeSelect: 'Vui lòng đánh dấu nội dung trước khi thay đổi kiểu dáng', + stylesNoStylesOnPage: 'Không có kiểu dáng nào đang hoạt động', + tableColMergeLeft: 'Vui lòng đặt con trỏ ở bên trái của hai ô bạn muốn gộp', + tableSplitNotSplittable: 'Bạn không thể tách một ô chưa được gộp.', + propertyHasErrors: 'Thuộc tính này không hợp lệ', + externalLoginError: 'Đăng nhập bên ngoài', + unauthorized: 'Bạn không được phép thực hiện hành động này', + userNotFound: 'Người dùng cục bộ không được tìm thấy trong cơ sở dữ liệu', + externalInfoNotFound: 'Máy chủ không thể liên lạc với nhà cung cấp đăng nhập bên ngoài', + externalLoginFailed: + 'Máy chủ không thể xác thực bạn với nhà cung cấp đăng nhập bên ngoài. Vui lòng đóng cửa sổ và thử lại.', + externalLoginSuccess: 'Bạn đã đăng nhập thành công. Bạn có thể đóng cửa sổ này.', + externalLoginRedirectSuccess: 'Bạn đã đăng nhập thành công. Bạn sẽ được chuyển hướng trong giây lát.', + }, + openidErrors: { + accessDenied: 'Từ chối truy cập', + invalidRequest: 'Yêu cầu không hợp lệ', + invalidClient: 'Client không hợp lệ', + invalidGrant: 'Grant không hợp lệ', + unauthorizedClient: 'Client không được phép', + unsupportedGrantType: 'Loại grant không được hỗ trợ', + invalidScope: 'Scope không hợp lệ', + serverError: 'Lỗi máy chủ', + temporarilyUnavailable: 'Dịch vụ tạm thời không khả dụng', + }, + general: { + about: 'Giới thiệu', + action: 'Thao tác', + actions: 'Thao tác', + add: 'Thêm', + alias: 'Alias', + all: 'Tất cả', + areyousure: 'Bạn có chắc không?', + back: 'Quay lại', + backToOverview: 'Quay lại tổng quan', + border: 'Đường viền', + by: 'bởi', + cancel: 'Hủy', + cellMargin: 'Cell margin', + choose: 'Chọn', + clear: 'Xóa', + close: 'Đóng', + closewindow: 'Đóng cửa sổ', + closepane: 'Đóng ngăn', + comment: 'Bình luận', + confirm: 'Xác nhận', + constrain: 'Giới hạn', + constrainProportions: 'Giới hạn tỷ lệ', + content: 'Nội dung', + continue: 'Tiếp tục', + copy: 'Sao chép', + create: 'Tạo mới', + database: 'Cơ sở dữ liệu', + date: 'Ngày', + default: 'Mặc định', + delete: 'Xóa', + deleted: 'Đã xóa', + deleting: 'Đang xóa...', + design: 'Thiết kế', + details: 'Chi tiết', + dictionary: 'Từ điển', + dimensions: 'Kích thước', + discard: 'Bỏ', + document: 'Tài liệu', + down: 'Xuống', + download: 'Tải xuống', + edit: 'Chỉnh sửa', + edited: 'Đã chỉnh sửa', + elements: 'Các thành phần', + email: 'Email', + error: 'Lỗi', + field: 'Trường', + fieldFor: 'Trường cho %0%', + toggleFor: 'Chuyển đổi cho %0%', + findDocument: 'Tìm kiếm', + first: 'Đầu tiên', + focalPoint: 'Điểm trọng tâm', + general: 'Tổng quan', + groups: 'Nhóm', + group: 'Nhóm', + height: 'Chiều cao', + help: 'Trợ giúp', + hide: 'Ẩn', + history: 'Lịch sử', + icon: 'Biểu tượng', + id: 'Id', + import: 'Nhập khẩu', + excludeFromSubFolders: 'Tìm kiếm chỉ trong thư mục này', + info: 'Thông tin', + innerMargin: 'Inner margin', + insert: 'Chèn', + install: 'Cài đặt', + invalid: 'Không hợp lệ', + justify: 'Căn chỉnh', + label: 'Nhãn', + language: 'Ngôn ngữ', + last: 'Cuối cùng', + layout: 'Bố cục', + links: 'Liên kết', + loading: 'Đang tải...', + locked: 'Đã khóa', + login: 'Đăng nhập', + logoff: 'Đăng xuất', + logout: 'Đăng xuất', + macro: 'Macro', + mandatory: 'Bắt buộc', + manifest: 'Manifest', + message: 'Tin nhắn', + move: 'Di chuyển', + name: 'Tên', + never: 'Không bao giờ', + new: 'Mới', + next: 'Tiếp theo', + no: 'Không', + nodeName: 'Tên nút', + notFound: 'Không tìm thấy', + of: 'của', + off: 'Tắt', + ok: 'OK', + open: 'Mở', + options: 'Tùy chọn', + on: 'Bật', + or: 'hoặc', + orderBy: 'Sắp xếp theo', + password: 'Mật khẩu', + path: 'Đường dẫn', + pixels: 'pixels', + pleasewait: 'Vui lòng chờ trong giây lát...', + previous: 'Trước', + properties: 'Thuộc tính', + readMore: 'Đọc thêm', + rebuild: 'Xây dựng lại', + reciept: 'Email để nhận dữ liệu biểu mẫu', + recycleBin: 'Thùng rác', + recycleBinEmpty: 'Thùng rác của bạn đang trống', + reload: 'Tải lại', + remaining: 'Còn lại', + remove: 'Xóa', + rename: 'Đổi tên', + renew: 'Làm mới', + required: 'Bắt buộc', + retrieve: 'Lấy lại', + retry: 'Thử lại', + rights: 'Quyền', + scheduledPublishing: 'Lên lịch xuất bản', + umbracoInfo: 'Thông tin Umbraco', + search: 'Tìm kiếm', + searchNoResult: 'Xin lỗi, chúng tôi không thể tìm thấy những gì bạn đang tìm kiếm.', + noItemsInList: 'Không có mục nào được thêm vào', + server: 'Máy chủ', + settings: 'Cài đặt', + shared: 'Chia sẻ', + show: 'Hiện', + showPageOnSend: 'Hiện trang khi gửi', + size: 'Kích thước', + sort: 'Sắp xếp', + status: 'Trạng thái', + submit: 'Gửi', + success: 'Thành công', + type: 'Loại', + typeName: 'Tên loại', + typeToSearch: 'Nhập để tìm kiếm...', + unknown: 'Không xác định', + unknownUser: 'Người dùng không xác định', + under: 'dưới', + unnamed: 'Không tên', + up: 'Lên', + update: 'Cập nhật', + upgrade: 'Nâng cấp', + upload: 'Tải lên', + url: 'URL', + user: 'Người dùng', + users: 'Người dùng', + username: 'Tên người dùng', + value: 'Giá trị', + view: 'Xem', + welcome: 'Chào mừng...', + width: 'Chiều rộng', + yes: 'Có', + folder: 'Thư mục', + searchResults: 'Kết quả tìm kiếm', + reorder: 'Sắp xếp lại', + reorderDone: 'Tôi đã hoàn tất việc sắp xếp lại', + preview: 'Xem trước', + changePassword: 'Đổi mật khẩu', + to: 'đến', + listView: 'Chế độ xem danh sách', + saving: 'Đang lưu...', + current: 'hiện tại', + embed: 'Nhúng', + addEditLink: 'Thêm/Chỉnh sửa liên kết', + removeLink: 'Xóa liên kết', + mediaPicker: 'Chọn phương tiện', + viewSourceCode: 'Xem mã nguồn', + selected: 'đã chọn', + other: 'Khác', + articles: 'Bài viết', + videos: 'Video', + avatar: 'Avatar cho', + header: 'Tiêu đề', + systemField: 'Trường hệ thống', + lastUpdated: 'Cập nhật lần cuối', + selectAll: 'Chọn tất cả', + skipToMenu: 'Chuyển đến menu', + skipToContent: 'Chuyển đến nội dung', + readOnly: 'Chỉ đọc', + restore: 'Khôi phục', + primary: 'Chính', + change: 'Thay đổi', + cropSection: 'Cắt phần', + generic: 'Chung', + media: 'Phương tiện', + revert: 'Hoàn tác', + validate: 'Xác thực', + newVersionAvailable: 'Có phiên bản mới', + duration: (duration: string, date: Date | string, now: Date | string) => { + if (new Date(date).getTime() < new Date(now).getTime()) return `${duration} trước`; + return `trong ${duration}`; + }, + }, + colors: { + blue: 'Xanh dương', + }, + shortcuts: { + addGroup: 'Thêm nhóm', + addProperty: 'Thêm thuộc tính', + addEditor: 'Thêm trình chỉnh sửa', + addTemplate: 'Thêm mẫu', + addChildNode: 'Thêm node con', + addChild: 'Thêm con', + editDataType: 'Chỉnh sửa kiểu dữ liệu', + navigateSections: 'Điều hướng các phần', + shortcut: 'Phím tắt', + showShortcuts: 'hiển thị phím tắt', + toggleListView: 'Chuyển đổi chế độ xem danh sách', + toggleAllowAsRoot: 'Cho phép làm gốc', + commentLine: 'Bình luận/Bỏ bình luận dòng', + removeLine: 'Xóa dòng', + copyLineUp: 'Sao chép dòng lên trên', + copyLineDown: 'Sao chép dòng xuống dưới', + moveLineUp: 'Di chuyển dòng lên trên', + moveLineDown: 'Di chuyển dòng xuống dưới', + generalHeader: 'Chung', + editorHeader: 'Trình chỉnh sửa', + toggleAllowCultureVariants: 'Cho phép biến thể ngôn ngữ', + addTab: 'Thêm tab', + }, + graphicheadline: { + backgroundcolor: 'Màu nền', + bold: 'Đậm', + color: 'Màu chữ', + font: 'Phông chữ', + text: 'Văn bản', + }, + globalSearch: { + navigateSearchProviders: 'Điều hướng nhà cung cấp tìm kiếm', + navigateSearchResults: 'Điều hướng kết quả tìm kiếm', + }, + headers: { + page: 'Trang', + }, + installer: { + databaseErrorCannotConnect: 'Trình cài đặt không thể kết nối tới cơ sở dữ liệu.', + databaseErrorWebConfig: 'Không thể lưu tệp web.config. Vui lòng chỉnh sửa chuỗi kết nối thủ công.', + databaseFound: 'Cơ sở dữ liệu của bạn đã được tìm thấy và được nhận diện là', + databaseHeader: 'Cấu hình cơ sở dữ liệu', + databaseInstall: 'Nhấn nút cài đặt để cài đặt cơ sở dữ liệu Umbraco %0%', + databaseInstallDone: + 'Umbraco %0% đã được sao chép vào cơ sở dữ liệu của bạn. Nhấn Tiếp theo để tiếp tục.', + databaseNotFound: + '

Không tìm thấy cơ sở dữ liệu! Vui lòng kiểm tra lại thông tin trong "connection string" của tệp "web.config".

Để tiếp tục, hãy chỉnh sửa tệp "web.config" (sử dụng Visual Studio hoặc trình soạn thảo yêu thích của bạn), cuộn xuống cuối, thêm chuỗi kết nối cho cơ sở dữ liệu của bạn vào khóa có tên "UmbracoDbDSN" và lưu tệp.

Nhấn nút thử lại khi hoàn tất.
Xem thêm thông tin về chỉnh sửa web.config tại đây.

', + databaseText: + 'Để hoàn tất bước này, bạn cần biết một số thông tin về máy chủ cơ sở dữ liệu của mình ("connection string").
Vui lòng liên hệ với nhà cung cấp dịch vụ (ISP) nếu cần. Nếu bạn đang cài đặt trên máy tính hoặc máy chủ nội bộ, bạn có thể cần thông tin từ quản trị viên hệ thống.', + databaseUpgrade: + '

Nhấn nút nâng cấp để cập nhật cơ sở dữ liệu của bạn lên Umbraco %0%

Đừng lo - không có nội dung nào bị xóa và mọi thứ sẽ tiếp tục hoạt động sau đó!

', + databaseUpgradeDone: + 'Cơ sở dữ liệu của bạn đã được nâng cấp lên phiên bản cuối cùng %0%.
Nhấn Tiếp theo để tiếp tục.', + databaseUpToDate: + 'Cơ sở dữ liệu hiện tại của bạn đã được cập nhật mới nhất! Nhấp tiếp theo để tiếp tục trình hướng dẫn cấu hình.', + defaultUserChangePass: 'Mật khẩu của người dùng mặc định cần được thay đổi!', + defaultUserDisabled: + 'Người dùng mặc định đã bị vô hiệu hóa hoặc không có quyền truy cập Umbraco!

Không cần thực hiện thêm hành động nào khác. Nhấn Tiếp theo để tiếp tục.', + defaultUserPassChanged: + 'Mật khẩu của người dùng mặc định đã được thay đổi thành công kể từ khi cài đặt!

Không cần thực hiện thêm hành động nào khác. Nhấn Tiếp theo để tiếp tục.', + defaultUserPasswordChanged: 'Mật khẩu đã được thay đổi!', + greatStart: 'Bắt đầu thuận lợi, hãy xem video giới thiệu của chúng tôi', + licenseText: + 'Bằng cách nhấn nút tiếp theo (hoặc chỉnh sửa giá trị umbracoConfigurationStatus trong web.config), bạn chấp nhận giấy phép phần mềm được nêu trong hộp dưới đây. Lưu ý rằng bản phân phối Umbraco này bao gồm hai giấy phép khác nhau: giấy phép MIT mã nguồn mở cho framework và giấy phép phần mềm miễn phí Umbraco cho giao diện người dùng.', + None: 'Chưa được cài đặt.', + permissionsAffectedFolders: 'Các tệp và thư mục bị ảnh hưởng', + permissionsAffectedFoldersMoreInfo: 'Thông tin thêm về thiết lập quyền cho Umbraco tại đây', + permissionsAffectedFoldersText: 'Bạn cần cấp quyền sửa đổi ASP.NET cho các tệp/thư mục sau', + permissionsAlmostPerfect: + 'Cài đặt quyền của bạn gần như hoàn hảo!

Bạn có thể chạy Umbraco mà không gặp vấn đề gì, nhưng sẽ không thể cài đặt các gói mở rộng được khuyến nghị để khai thác tối đa Umbraco.', + permissionsHowtoResolve: 'Cách khắc phục', + permissionsHowtoResolveLink: 'Nhấn vào đây để đọc phiên bản văn bản', + permissionsHowtoResolveText: + 'Xem video hướng dẫn về thiết lập quyền thư mục cho Umbraco hoặc đọc phiên bản văn bản.', + permissionsMaybeAnIssue: + 'Cài đặt quyền của bạn có thể gây ra vấn đề!

Bạn có thể chạy Umbraco mà không gặp sự cố, nhưng sẽ không thể tạo thư mục hoặc cài đặt gói mở rộng được khuyến nghị để tận dụng tối đa Umbraco.', + permissionsNotReady: + 'Cài đặt quyền của bạn chưa sẵn sàng cho Umbraco!

Để chạy Umbraco, bạn cần cập nhật cài đặt quyền.', + permissionsPerfect: + 'Cài đặt quyền của bạn hoàn hảo!

Bạn đã sẵn sàng để chạy Umbraco và cài đặt gói mở rộng!', + permissionsResolveFolderIssues: 'Đang xử lý sự cố thư mục', + permissionsResolveFolderIssuesLink: + 'Theo liên kết này để biết thêm thông tin về sự cố với ASP.NET và việc tạo thư mục', + permissionsSettingUpPermissions: 'Thiết lập quyền thư mục', + permissionsText: + 'Umbraco cần quyền ghi/sửa đổi đối với một số thư mục nhất định để lưu trữ các tệp như hình ảnh và PDF. Nó cũng lưu trữ dữ liệu tạm (cache) để cải thiện hiệu suất của trang web.', + runwayFromScratch: 'Tôi muốn bắt đầu từ đầu', + runwayFromScratchText: + 'Trang web của bạn hiện tại hoàn toàn trống, điều này rất phù hợp nếu bạn muốn bắt đầu từ đầu và tự tạo Loại Tài liệu (Document Types) và mẫu (templates) của riêng mình. (tìm hiểu cách thực hiện) Bạn vẫn có thể chọn cài đặt Runway sau này. Hãy vào phần Developer và chọn Packages.', + runwayHeader: 'Bạn vừa thiết lập một nền tảng Umbraco sạch. Bạn muốn làm gì tiếp theo?', + runwayInstalled: 'Runway đã được cài đặt', + runwayInstalledText: + 'Bạn đã có sẵn nền tảng. Hãy chọn các mô-đun bạn muốn cài đặt thêm.
Đây là danh sách mô-đun được khuyến nghị, hãy đánh dấu các mô-đun bạn muốn cài đặt hoặc xem danh sách đầy đủ các mô-đun', + runwayOnlyProUsers: 'Chỉ khuyến nghị cho người dùng có kinh nghiệm', + runwaySimpleSite: 'Tôi muốn bắt đầu với một trang web đơn giản', + runwaySimpleSiteText: + '

"Runway" là một trang web đơn giản cung cấp một số Loại Tài liệu và mẫu cơ bản. Trình cài đặt có thể thiết lập Runway cho bạn một cách tự động, nhưng bạn hoàn toàn có thể chỉnh sửa, mở rộng hoặc xóa nó. Runway không bắt buộc và bạn có thể dùng Umbraco mà không cần nó. Tuy nhiên, Runway cung cấp một nền tảng dễ dàng dựa trên các phương pháp hay nhất để giúp bạn bắt đầu nhanh hơn. Nếu chọn cài đặt Runway, bạn có thể chọn thêm các khối xây dựng cơ bản gọi là Runway Modules để mở rộng các trang Runway của mình.

Kèm theo Runway: Trang chủ, Trang Bắt đầu, Trang Cài đặt Mô-đun.
Mô-đun tùy chọn: Thanh điều hướng trên, Sơ đồ trang, Liên hệ, Thư viện ảnh.
', + runwayWhatIsRunway: 'Runway là gì', + step1: 'Bước 1/5: Chấp nhận giấy phép', + step2: 'Bước 2/5: Cấu hình cơ sở dữ liệu', + step3: 'Bước 3/5: Xác thực quyền truy cập tệp', + step4: 'Bước 4/5: Kiểm tra bảo mật Umbraco', + step5: 'Bước 5/5: Umbraco sẵn sàng để bạn bắt đầu', + thankYou: 'Cảm ơn bạn đã chọn Umbraco', + theEndBrowseSite: + '

Duyệt trang web mới của bạn

Bạn đã cài đặt Runway, vậy tại sao không xem thử trang web mới của mình trông như thế nào.', + theEndFurtherHelp: + '

Hỗ trợ và thông tin thêm

\nNhận trợ giúp từ cộng đồng đạt giải thưởng của chúng tôi, duyệt tài liệu hoặc xem một số video miễn phí về cách xây dựng một trang web đơn giản, cách sử dụng gói và hướng dẫn nhanh về các thuật ngữ trong Umbraco.', + theEndHeader: 'Umbraco %0% đã được cài đặt và sẵn sàng sử dụng', + theEndInstallFailed: + "Để hoàn tất cài đặt, bạn cần chỉnh sửa thủ công /web.config file và cập nhật khóa AppSetting UmbracoConfigurationStatus ở cuối với giá trị '%0%'.", + theEndInstallSuccess: + 'Bạn có thể bắt đầu ngay lập tức bằng cách nhấp nút "Khởi chạy Umbraco" bên dưới.
Nếu bạn mới sử dụng Umbraco, bạn có thể tìm thấy rất nhiều tài nguyên trong trang bắt đầu.', + theEndOpenUmbraco: + '

Khởi chạy Umbraco

\nĐể quản lý trang web, chỉ cần mở backoffice của Umbraco và bắt đầu thêm nội dung, cập nhật template và stylesheet hoặc thêm chức năng mới.', + Unavailable: 'Kết nối cơ sở dữ liệu thất bại.', + Version3: 'Umbraco Phiên bản 3', + Version4: 'Umbraco Phiên bản 4', + watch: 'Xem', + welcomeIntro: + 'Trình hướng dẫn này sẽ dẫn bạn qua quá trình cấu hình Umbraco %0% cho bản cài đặt mới hoặc nâng cấp từ phiên bản 3.0.

Nhấn "tiếp theo" để bắt đầu.', + }, + language: { + cultureCode: 'Mã ngôn ngữ', + displayName: 'Tên ngôn ngữ', + noFallbackLanguages: 'Không có ngôn ngữ khác để lựa chọn', + }, + lockout: { + lockoutWillOccur: 'Bạn đã không hoạt động, hệ thống sẽ tự động đăng xuất sau', + renewSession: 'Gia hạn ngay để lưu công việc của bạn', + }, + timeout: { + warningHeadline: 'Hết hạn phiên', + warningText: 'Phiên làm việc của bạn sắp hết hạn và bạn sẽ bị đăng xuất trong {0} giây.', + warningLogoutAction: 'Đăng xuất', + warningContinueAction: 'Tiếp tục đăng nhập', + }, + login: { + greeting0: 'Chào mừng', + greeting1: 'Chào mừng', + greeting2: 'Chào mừng', + greeting3: 'Chào mừng', + greeting4: 'Chào mừng', + greeting5: 'Chào mừng', + greeting6: 'Chào mừng', + instruction: 'Đăng nhập vào Umbraco', + signInWith: 'Đăng nhập bằng {0}', + timeout: 'Phiên làm việc của bạn đã hết hạn. Vui lòng đăng nhập lại bên dưới.', + }, + main: { + dashboard: 'Bảng điều khiển', + sections: 'Khu vực', + tree: 'Nội dung', + }, + moveOrCopy: { + choose: 'Chọn trang bên trên...', + copyDone: '%0% đã được sao chép vào %1%', + copyTo: 'Chọn nơi mà tài liệu %0% sẽ được sao chép đến bên dưới', + moveDone: '%0% đã được di chuyển vào %1%', + moveTo: 'Chọn nơi mà tài liệu %0% sẽ được di chuyển đến bên dưới', + nodeSelected: "đã được chọn làm gốc cho nội dung mới của bạn, hãy nhấn 'ok' bên dưới.", + noNodeSelected: "Chưa chọn nút nào, vui lòng chọn một nút trong danh sách bên trên trước khi nhấn 'ok'", + notAllowedByContentType: 'Nút hiện tại không được phép nằm dưới nút đã chọn do khác loại nội dung', + notAllowedByPath: + 'Nút hiện tại không thể di chuyển vào một trong các trang con của nó, và cũng không thể để cha và đích là cùng một nút', + notAllowedAtRoot: 'Nút hiện tại không thể tồn tại ở cấp gốc', + notValid: 'Hành động không được phép vì bạn không đủ quyền trên một hoặc nhiều tài liệu con.', + relateToOriginal: 'Liên kết các mục đã sao chép với bản gốc', + }, + notifications: { + editNotifications: 'Chọn thông báo cho %0%', + notificationsSavedFor: 'Đã lưu cài đặt thông báo cho %0%', + notifications: 'Thông báo', + }, + packager: { + actions: 'Actions', + created: 'Ngày tạo', + createPackage: 'Tạo gói', + chooseLocalPackageText: + 'Chọn gói từ máy của bạn bằng cách nhấp vào nút Duyệt
và tìm đến gói cần cài đặt. Các gói Umbraco thường có phần mở rộng ".umb" hoặc ".zip".', + deletewarning: 'Thao tác này sẽ xóa gói', + includeAllChildNodes: 'Bao gồm tất cả các nút con', + installed: 'Đã cài đặt', + installedPackages: 'Các gói đã cài đặt', + installInstructions: 'Hướng dẫn cài đặt', + noConfigurationView: 'Gói này không có giao diện cấu hình', + noPackagesCreated: 'Chưa có gói nào được tạo', + noPackages: 'Chưa có gói nào được cài đặt', + noPackagesDescription: + "Duyệt qua các gói có sẵn bằng cách sử dụng biểu tượng 'Gói mở rộng' ở góc trên bên phải màn hình của bạn", + packageContent: 'Nội dung gói', + packageLicense: 'Giấy phép', + packageSearch: 'Tìm kiếm gói', + packageSearchResults: 'Kết quả cho', + packageNoResults: 'Chúng tôi không thể tìm thấy bất kỳ điều gì cho', + packageNoResultsDescription: 'Vui lòng thử tìm kiếm một gói khác hoặc duyệt qua các danh mục', + packagesPopular: 'Phổ biến', + packagesPromoted: 'Được quảng bá', + packagesNew: 'Phiên bản mới', + packageHas: 'có', + packageKarmaPoints: 'điểm karma', + packageInfo: 'Thông tin', + packageOwner: 'Chủ sở hữu', + packageContrib: 'Người đóng góp', + packageCreated: 'Ngày tạo', + packageCurrentVersion: 'Phiên bản hiện tại', + packageNetVersion: '.NET version', + packageDownloads: 'Tải xuống', + packageLikes: 'Lượt thích', + packageCompatibility: 'Tương thích', + packageCompatibilityDescription: + 'Gói này tương thích với các phiên bản Umbraco sau, theo báo cáo từ cộng đồng. Không thể đảm bảo khả năng tương thích hoàn toàn đối với các phiên bản có tỷ lệ báo cáo dưới 100%', + packageExternalSources: 'Nguồn bên ngoài', + packageAuthor: 'Tác giả', + packageDocumentation: 'Tài liệu', + packageMetaData: 'Thông tin meta gói', + packageName: 'Tên gói', + packageNoItemsHeader: 'Gói không chứa bất kỳ mục nào', + packageNoItemsText: + 'File gói này không chứa bất kỳ mục nào để gỡ cài đặt.

Bạn có thể xóa gói này khỏi hệ thống bằng cách nhấn "gỡ cài đặt gói" bên dưới.', + packageOptions: 'Tùy chọn gói', + packageMigrationsRun: 'Chạy các migration gói đang chờ', + packageMigrationsConfirmText: 'Bạn có muốn chạy các migration gói đang chờ không?', + packageMigrationsComplete: 'Các migration gói đã hoàn tất thành công.', + packageMigrationsNonePending: 'Tất cả migration gói đã hoàn tất thành công.', + packageReadme: 'Hướng dẫn đọc gói', + packageRepository: 'Kho gói', + packageUninstallConfirm: 'Xác nhận gỡ cài đặt gói', + packageUninstalledHeader: 'Gói đã được gỡ', + packageUninstalledText: 'Gói đã được gỡ thành công', + packageUninstallHeader: 'Gỡ cài đặt gói', + packageUninstallText: + 'Bạn có thể bỏ chọn các mục bạn không muốn xóa bên dưới. Khi nhấn "xác nhận gỡ cài đặt", tất cả mục được đánh dấu sẽ bị xóa.
Lưu ý: bất kỳ tài liệu, media... phụ thuộc vào các mục bị xóa sẽ ngừng hoạt động và có thể gây ra sự không ổn định cho hệ thống, vì vậy hãy gỡ cài đặt cẩn thận. Nếu nghi ngờ, hãy liên hệ tác giả gói.', + packageVersion: 'Phiên bản gói', + verifiedToWorkOnUmbracoCloud: 'Đã xác minh hoạt động trên Umbraco Cloud', + }, + paste: { + doNothing: 'Dán kèm đầy đủ định dạng (Không khuyến nghị)', + errorMessage: + 'Văn bản bạn đang cố dán chứa các ký tự hoặc định dạng đặc biệt. Điều này có thể do việc sao chép văn bản từ Microsoft Word. Umbraco có thể tự động loại bỏ các ký tự hoặc định dạng đặc biệt, vì vậy nội dung được dán sẽ phù hợp hơn với web.', + removeAll: 'Dán dưới dạng văn bản thô mà không có bất kỳ định dạng nào', + removeSpecialFormattering: 'Dán, nhưng loại bỏ định dạng (Khuyến nghị)', + }, + publicAccess: { + paGroups: 'Bảo vệ dựa trên nhóm', + paGroupsHelp: 'Nếu bạn muốn cấp quyền truy cập cho tất cả các thành viên của các nhóm thành viên cụ thể', + paGroupsNoGroups: 'Bạn cần tạo một nhóm thành viên trước khi có thể sử dụng xác thực dựa trên nhóm', + paErrorPage: 'Trang gặp sự cố', + paErrorPageHelp: 'Được sử dụng khi người dùng đã đăng nhập nhưng không có quyền truy cập', + paHowWould: 'Chọn cách hạn chế quyền truy cập vào trang %0%', + paIsProtected: '%0% hiện đang được bảo vệ', + paIsRemoved: 'Bảo vệ đã được gỡ bỏ khỏi %0%', + paLoginPage: 'Trang đăng nhập', + paLoginPageHelp: 'Chọn trang chứa biểu mẫu đăng nhập', + paRemoveProtection: 'Gỡ bỏ bảo vệ...', + paRemoveProtectionConfirm: 'Bạn có chắc chắn muốn gỡ bỏ bảo vệ khỏi trang %0% không?', + paSelectPages: 'Chọn các trang chứa biểu mẫu đăng nhập và thông báo lỗi', + paSelectGroups: 'Chọn các nhóm có quyền truy cập vào trang %0%', + paSelectMembers: 'Chọn các thành viên có quyền truy cập vào trang %0%', + paMembers: 'Bảo vệ theo thành viên cụ thể', + paMembersHelp: 'Nếu bạn muốn cấp quyền truy cập cho các thành viên cụ thể', + }, + publish: { + contentPublishedFailedIsTrashed: '%0% không thể được xuất bản vì mục đang ở trong thùng rác.', + contentPublishedFailedAwaitingRelease: '%0% không thể được xuất bản vì mục đang chờ phát hành.', + contentPublishedFailedExpired: '%0% không thể được xuất bản vì mục đã hết hạn.', + contentPublishedFailedInvalid: + '%0% không thể xuất bản vì các thuộc tính sau: %1% không đáp ứng các quy tắc xác thực.', + contentPublishedFailedByEvent: '%0% không thể được xuất bản, một tiện ích mở rộng bên thứ ba đã hủy bỏ hành động.', + contentPublishedFailedByParent: '%0% không thể được xuất bản, vì một trang cha không được xuất bản.', + contentPublishedFailedByMissingName: '%0% không thể được xuất bản, vì thiếu tên.', + includeUnpublished: 'Bao gồm các trang con chưa được xuất bản', + inProgress: 'Đang xuất bản - vui lòng chờ...', + inProgressCounter: '%0% trong số %1% trang đã được xuất bản...', + nodePublish: '%0% đã được xuất bản', + nodePublishAll: '%0% và các trang con đã được xuất bản', + publishAll: 'Xuất bản %0% và tất cả các trang con của nó', + publishHelp: + 'Nhấn Xuất bản để xuất bản %0% và làm cho nội dung này khả dụng công khai.

Bạn có thể xuất bản trang này và tất cả các trang con của nó bằng cách chọn Bao gồm các trang con chưa xuất bản bên dưới.', + invalidPublishBranchPermissions: 'Người dùng không có đủ quyền để xuất bản tất cả tài liệu con', + contentPublishedFailedReqCultureValidationError: + "Xác thực không thành công đối với ngôn ngữ bắt buộc '%0%'. Ngôn ngữ này đã được lưu nhưng chưa được xuất bản.", + }, + colorpicker: { + noColors: 'Bạn chưa cấu hình bất kỳ màu nào được phê duyệt', + }, + colorPickerConfigurations: { + colorsTitle: 'Màu sắc', + colorsDescription: 'Thêm, xóa hoặc sắp xếp màu sắc', + showLabelTitle: 'Có bao gồm nhãn không?', + showLabelDescription: + 'Lưu trữ màu sắc dưới dạng một đối tượng JSON chứa cả chuỗi hex màu và nhãn, thay vì chỉ là chuỗi hex.', + }, + contentPicker: { + allowedItemTypes: 'Bạn chỉ có thể chọn các mục có loại: %0%', + pickedTrashedItem: 'Bạn đã chọn một mục nội dung hiện đang bị xóa hoặc trong thùng rác', + pickedTrashedItems: 'Bạn đã chọn các mục nội dung hiện đang bị xóa hoặc trong thùng rác', + specifyPickerRootTitle: 'Chỉ định gốc', + defineRootNode: 'Chọn nút gốc', + defineXPathOrigin: 'Chỉ định qua XPath', + defineDynamicRoot: 'Chỉ định một gốc động', + unsupportedHeadline: (type?: string) => + `"Không được hỗ trợ ${type ?? 'nội dung'}
Nội dung sau đây không còn được hỗ trợ trong Trình soạn thảo này".`, + unsupportedMessage: + 'Nếu bạn vẫn cần nội dung này, vui lòng liên hệ với quản trị viên của bạn. Nếu không, bạn có thể xóa nó.', + unsupportedRemove: 'Xóa các mục không được hỗ trợ?', + }, + dynamicRoot: { + configurationTitle: 'Truy vấn Gốc Động', + pickDynamicRootOriginTitle: 'Chọn nguồn gốc', + pickDynamicRootOriginDesc: 'Định nghĩa nguồn gốc cho truy vấn Dynamic Root của bạn', + originRootTitle: 'Gốc', + originRootDesc: 'Nút gốc của phiên chỉnh sửa này', + originParentTitle: 'Cha', + originParentDesc: 'Nút cha của nguồn trong phiên chỉnh sửa này', + originCurrentTitle: 'Hiện tại', + originCurrentDesc: 'Nút nội dung là nguồn cho phiên chỉnh sửa này', + originSiteTitle: 'Site', + originSiteDesc: 'Tìm nút gần nhất với tên máy chủ', + originByKeyTitle: 'Nút Cụ Thể', + originByKeyDesc: 'Chọn một Nút cụ thể làm nguồn cho truy vấn này', + pickDynamicRootQueryStepTitle: 'Thêm bước vào truy vấn', + pickDynamicRootQueryStepDesc: 'Định nghĩa bước tiếp theo của truy vấn Dynamic Root của bạn', + queryStepNearestAncestorOrSelfTitle: 'Tổ Tiên Gần Nhất Hoặc Chính Nó', + queryStepNearestAncestorOrSelfDesc: + 'Truy vấn tổ tiên gần nhất hoặc chính nó phù hợp với một trong các loại đã cấu hình', + queryStepFurthestAncestorOrSelfTitle: 'Tổ Tiên Xa Nhất Hoặc Chính Nó', + queryStepFurthestAncestorOrSelfDesc: + 'Truy vấn tổ tiên xa nhất hoặc chính nó phù hợp với một trong các loại đã cấu hình', + queryStepNearestDescendantOrSelfTitle: 'Tổ Tiên Gần Nhất Hoặc Chính Nó', + queryStepNearestDescendantOrSelfDesc: + 'Truy vấn phần tử con gần nhất hoặc chính nó phù hợp với một trong các loại đã được cấu hình.', + queryStepFurthestDescendantOrSelfTitle: 'Tổ Tiên Xa Nhất Hoặc Chính Nó', + queryStepFurthestDescendantOrSelfDesc: + 'Truy vấn phần tử con xa nhất hoặc chính nó phù hợp với một trong các loại đã được cấu hình.', + queryStepCustomTitle: 'Tùy Chỉnh', + queryStepCustomDesc: 'Truy vấn bằng cách sử dụng Bước Truy Vấn Tùy Chỉnh', + addQueryStep: 'Thêm bước truy vấn', + queryStepTypes: 'Phù hợp với các loại: ', + noValidStartNodeTitle: 'Không có nội dung phù hợp', + noValidStartNodeDesc: + 'Cấu hình của thuộc tính này không khớp với bất kỳ nội dung nào. Tạo nội dung bị thiếu hoặc liên hệ với quản trị viên của bạn để điều chỉnh cài đặt Gốc Động cho thuộc tính này.', + cancelAndClearQuery: 'Hủy và xóa truy vấn', + }, + mediaPicker: { + deletedItem: 'Mục đã xóa', + pickedTrashedItem: 'Bạn đã chọn một mục phương tiện hiện đang bị xóa hoặc trong thùng rác', + pickedTrashedItems: 'Bạn đã chọn các mục phương tiện hiện đang bị xóa hoặc trong thùng rác', + trashed: 'Đã xóa', + openMedia: 'Mở trong Thư viện Phương tiện', + changeMedia: 'Thay đổi Mục Phương tiện', + editMediaEntryLabel: 'Chỉnh sửa %0% trên %1%', + confirmCancelMediaEntryCreationHeadline: 'Bỏ qua việc tạo?', + confirmCancelMediaEntryCreationMessage: 'Bạn có chắc chắn muốn hủy việc tạo không?', + confirmCancelMediaEntryHasChanges: + 'Bạn đã thực hiện các thay đổi đối với nội dung này. Bạn có chắc chắn muốn bỏ qua chúng không?', + confirmRemoveAllMediaEntryMessage: 'Xóa tất cả phương tiện?', + tabClipboard: 'Clipboard', + notAllowed: 'Không được phép', + openMediaPicker: 'Mở trình chọn phương tiện', + }, + propertyEditorPicker: { + title: 'Chọn sửa thuộc tính', + openPropertyEditorPicker: 'Chọn giao diện trình chỉnh sửa thuộc tính', + }, + relatedlinks: { + enterExternal: 'Nhập liên kết bên ngoài', + chooseInternal: 'Chọn trang nội bộ', + caption: 'Chú thích', + link: 'Liên kết', + newWindow: 'Mở trong cửa sổ mới', + captionPlaceholder: 'Nhập chú thích hiển thị', + externalLinkPlaceholder: 'Nhập liên kết bên ngoài', + }, + imagecropper: { + reset: 'Đặt lại cắt', + updateEditCrop: 'Xong', + undoEditCrop: 'Hoàn tác chỉnh sửa', + customCrop: 'Người dùng xác định', + }, + rollback: { + changes: 'Thay đổi', + created: 'Đã tạo', + currentVersion: 'Phiên bản hiện tại', + diffHelp: + 'Điều này hiển thị sự khác biệt giữa phiên bản hiện tại (bản nháp) và phiên bản được chọn.
Văn bản màu đỏ sẽ bị xóa trong phiên bản được chọn, văn bản màu xanh sẽ được thêm vào.', + noDiff: 'Không có sự khác biệt nào giữa phiên bản hiện tại (bản nháp) và phiên bản được chọn', + documentRolledBack: 'Tài liệu đã được hoàn tác', + headline: 'Chọn một phiên bản để so sánh với phiên bản hiện tại', + htmlHelp: + 'Điều này hiển thị phiên bản đã chọn dưới dạng HTML, nếu bạn muốn xem sự khác biệt giữa 2 phiên bản cùng một lúc, hãy sử dụng chế độ xem khác biệt', + rollbackTo: 'Hoàn tác về', + selectVersion: 'Chọn phiên bản', + view: 'Xem', + pagination: 'Hiển thị %0% trong %1% tổng số %2% phiên bản', + versions: 'Phiên bản', + currentDraftVersion: 'Phiên bản nháp hiện tại', + currentPublishedVersion: 'Phiên bản đã xuất bản hiện tại', + }, + scripts: { + editscript: 'Sửa tệp script', + }, + sections: { + content: 'Nội dung', + media: 'Phương tiện', + member: 'Thành viên', + packages: 'Gói mở rộng', + marketplace: 'Marketplace', + settings: 'Cài đặt', + translation: 'Dịch thuật', + users: 'Người dùng', + }, + help: { + tours: 'Hướng dẫn', + theBestUmbracoVideoTutorials: 'Các video hướng dẫn Umbraco tốt nhất', + umbracoForum: 'Truy cập our.umbraco.com', + umbracoTv: 'Truy cập umbraco.tv', + umbracoLearningBase: 'Xem các video hướng dẫn miễn phí của chúng tôi', + umbracoLearningBaseDescription: 'trên Umbraco Learning Base', + }, + settings: { + defaulttemplate: 'Mẫu mặc định', + importDocumentTypeHelp: + 'Để nhập một loại tài liệu, hãy tìm tệp ".udt" trên máy tính của bạn bằng cách nhấp vào nút "Nhập" (bạn sẽ được yêu cầu xác nhận trên màn hình tiếp theo)', + newtabname: 'Tiêu đề Tab Mới', + nodetype: 'Loại nút', + objecttype: 'Loại đối tượng', + stylesheet: 'Stylesheet', + script: 'Script', + tab: 'Tab', + tabname: 'Tiêu đề Tab', + tabs: 'Các Tab', + contentTypeEnabled: 'Đã bật Loại nội dung gốc', + contentTypeUses: 'Loại nội dung này sử dụng', + noPropertiesDefinedOnTab: + 'Không có thuộc tính nào được định nghĩa trên tab này. Nhấp vào liên kết "thêm thuộc tính mới" ở trên cùng để tạo thuộc tính mới.', + createMatchingTemplate: 'Tạo mẫu tương ứng', + addIcon: 'Thêm biểu tượng', + }, + sort: { + sortOrder: 'Thứ tự sắp xếp', + sortCreationDate: 'Ngày tạo', + sortDone: 'Sắp xếp hoàn tất.', + sortHelp: + 'Kéo các mục khác nhau lên hoặc xuống bên dưới để đặt cách chúng nên được sắp xếp. Hoặc nhấp vào tiêu đề cột để sắp xếp toàn bộ bộ sưu tập các mục', + sortPleaseWait: 'Vui lòng chờ. Các mục đang được sắp xếp, điều này có thể mất một thời gian.', + sortEmptyState: 'Nút này không có nút con nào để sắp xếp', + }, + speechBubbles: { + validationFailedHeader: 'Xác thực', + validationFailedMessage: 'Các lỗi xác thực phải được sửa trước khi mục có thể được lưu', + operationFailedHeader: 'Thất bại', + operationSavedHeader: 'Đã lưu', + operationSavedHeaderReloadUser: 'Đã lưu. Để xem các thay đổi, vui lòng tải lại trình duyệt của bạn', + invalidUserPermissionsText: 'Quyền người dùng không đủ, không thể hoàn thành thao tác', + operationCancelledHeader: 'Đã hủy', + operationCancelledText: 'Thao tác đã bị hủy bởi một tiện ích bên thứ ba', + folderUploadNotAllowed: + 'Tệp này đang được tải lên như một phần của thư mục, nhưng việc tạo thư mục mới không được phép ở đây', + folderCreationNotAllowed: 'Việc tạo thư mục mới không được phép ở đây', + contentPublishedFailedByEvent: 'Tài liệu không thể được xuất bản, một tiện ích bên thứ ba đã hủy bỏ hành động', + contentTypeDublicatePropertyType: 'Loại thuộc tính đã tồn tại', + contentTypePropertyTypeCreated: 'Loại thuộc tính đã được tạo', + contentTypePropertyTypeCreatedText: 'Tên: %0%
Loại dữ liệu: %1%', + contentTypePropertyTypeDeleted: 'Loại thuộc tính đã bị xóa', + contentTypeSavedHeader: 'Loại tài liệu đã được lưu', + contentTypeTabCreated: 'Tab đã được tạo', + contentTypeTabDeleted: 'Tab đã bị xóa', + contentTypeTabDeletedText: 'Tab có id: %0% đã bị xóa', + cssErrorHeader: 'Stylesheet không được lưu', + cssSavedHeader: 'Stylesheet đã được lưu', + cssSavedText: 'Stylesheet đã được lưu mà không có lỗi', + dataTypeSaved: 'Datatype đã được lưu', + dictionaryItemSaved: 'Dictionary item đã được lưu', + editContentPublishedFailedByValidation: 'Tài liệu không thể được xuất bản, nhưng chúng tôi đã lưu nó cho bạn', + editContentPublishedFailedByParent: 'Tài liệu không thể được xuất bản, vì một trang cha không được xuất bản', + editContentPublishedHeader: 'Tài liệu đã được xuất bản', + editContentPublishedText: 'và hiện đang hiển thị trên trang web', + editContentUnpublishedHeader: 'Tài liệu đã bị hủy xuất bản', + editContentUnpublishedText: 'và không còn hiển thị trên trang web', + editVariantPublishedText: '%0% đã được xuất bản và hiện đang hiển thị trên trang web', + editVariantSavedText: '%0% đã được lưu', + editBlueprintSavedHeader: 'Document Blueprint đã được lưu', + editBlueprintSavedText: 'Các thay đổi đã được lưu thành công', + editContentSavedHeader: 'Tài liệu đã được lưu', + editContentSavedText: 'Nhớ xuất bản để làm cho các thay đổi hiển thị', + editContentSendToPublish: 'Đã gửi để phê duyệt', + editContentSendToPublishText: 'Các thay đổi đã được gửi để phê duyệt', + editMediaSaved: 'Media đã được lưu', + editMediaSavedText: 'Media đã được lưu mà không có lỗi', + editMemberSaved: 'Member đã được lưu', + editStylesheetPropertySaved: 'Stylesheet Property đã được lưu', + editStylesheetSaved: 'Stylesheet đã được lưu', + editTemplateSaved: 'Template đã được lưu', + editUserError: 'Lỗi khi lưu người dùng (kiểm tra nhật ký)', + editUserSaved: 'Người dùng đã được lưu', + editUserTypeSaved: 'User type đã được lưu', + editUserGroupSaved: 'User group đã được lưu', + editCulturesAndHostnamesSaved: 'Cultures and hostnames đã được lưu', + editCulturesAndHostnamesError: 'Lỗi khi lưu cultures and hostnames', + fileErrorHeader: 'File không được lưu', + fileErrorText: 'File không thể được lưu. Vui lòng kiểm tra quyền truy cập file', + fileSavedHeader: 'File đã được lưu', + fileSavedText: 'File đã được lưu mà không có lỗi', + languageSaved: 'Language đã được lưu', + mediaTypeSavedHeader: 'Media Type đã được lưu', + memberTypeSavedHeader: 'Member Type đã được lưu', + memberGroupSavedHeader: 'Member Group đã được lưu', + memberGroupNameDuplicate: 'Một Member Group khác với cùng tên đã tồn tại', + templateErrorHeader: 'Template không được lưu', + templateErrorText: 'Vui lòng đảm bảo rằng bạn không có 2 templates với cùng một alias', + templateSavedHeader: 'Template đã được lưu', + templateSavedText: 'Template đã được lưu mà không có lỗi!', + contentUnpublished: 'Nội dung đã bị hủy xuất bản', + contentMandatoryCultureUnpublished: + "Ngôn ngữ bắt buộc '%0%' đã bị hủy xuất bản. Tất cả các ngôn ngữ cho mục nội dung này hiện đã bị hủy xuất bản.", + partialViewSavedHeader: 'Partial view đã được lưu', + partialViewSavedText: 'Partial view đã được lưu mà không có lỗi!', + partialViewErrorHeader: 'Partial view không được lưu', + partialViewErrorText: 'Đã xảy ra lỗi khi lưu file.', + permissionsSavedFor: 'Permissions đã được lưu cho', + deleteUserGroupsSuccess: 'Đã xóa %0% user groups', + deleteUserGroupSuccess: '%0% đã bị xóa', + enableUsersSuccess: 'Đã kích hoạt %0% users', + disableUsersSuccess: 'Đã vô hiệu hóa %0% users', + enableUserSuccess: '%0% đã được kích hoạt', + disableUserSuccess: '%0% đã bị vô hiệu hóa', + setUserGroupOnUsersSuccess: 'User groups đã được thiết lập', + unlockUsersSuccess: 'Đã mở khóa %0% users', + unlockUserSuccess: '%0% đã được mở khóa', + memberExportedSuccess: 'Member đã được xuất ra file', + memberExportedError: 'Đã xảy ra lỗi khi xuất member', + deleteUserSuccess: 'User %0% đã bị xóa', + resendInviteHeader: 'Mời user', + resendInviteSuccess: 'Invitation đã được gửi lại cho %0%', + contentReqCulturePublishError: "Không thể xuất bản tài liệu vì ngôn ngữ bắt buộc '%0%' chưa được xuất bản", + documentTypeExportedSuccess: 'Document Type đã được xuất ra file', + documentTypeExportedError: 'Đã xảy ra lỗi khi xuất Document Type', + dictionaryItemExportedSuccess: 'Dictionary item(s) đã được xuất ra file', + dictionaryItemExportedError: 'Đã xảy ra lỗi khi xuất Dictionary item(s)', + dictionaryItemImported: 'Dictionary item(s) đã được nhập khẩu!', + publishWithNoDomains: + 'Các miền chưa được cấu hình cho trang đa ngôn ngữ. Vui lòng liên hệ quản trị viên, xem nhật ký để biết thêm thông tin.', + publishWithMissingDomain: + 'Không có miền nào được cấu hình cho %0%, vui lòng liên hệ quản trị viên, xem nhật ký để biết thêm thông tin', + copySuccessMessage: 'Thông tin hệ thống của bạn đã được sao chép thành công vào clipboard', + cannotCopyInformation: 'Không thể sao chép thông tin hệ thống của bạn vào clipboard', + webhookSaved: 'Webhook đã được lưu', + editMultiContentPublishedText: '%0% tài liệu đã được xuất bản và hiển thị trên website.', + editMultiContentUnpublishedText: '%0% tài liệu đã bị hủy xuất bản và không còn hiển thị trên website.', + editVariantUnpublishedText: '%0% đã bị hủy xuất bản và không còn hiển thị trên website.', + editMultiVariantPublishedText: '%0% tài liệu đã được xuất bản cho các ngôn ngữ %1% và đang hiển thị trên website.', + editMultiVariantUnpublishedText: + '%0% tài liệu đã bị hủy xuất bản cho các ngôn ngữ %1% và không còn hiển thị trên website.', + editContentScheduledSavedText: 'Lịch xuất bản đã được cập nhật', + editContentScheduledNotSavedText: 'Lịch xuất bản không thể được cập nhật', + editVariantSendToPublishText: '%0% thay đổi đã được gửi để phê duyệt', + contentCultureUnpublished: 'Biến thể nội dung %0% đã bị hủy xuất bản', + contentCultureValidationError: "Xác thực không thành công cho ngôn ngữ '%0%'", + scheduleErrReleaseDate1: 'Ngày phát hành không thể ở trong quá khứ', + scheduleErrReleaseDate2: "Không thể lên lịch xuất bản tài liệu vì '%0%' là bắt buộc chưa được xuất bản", + scheduleErrReleaseDate3: + "Không thể lên lịch xuất bản tài liệu vì '%0%' là bắt buộc có ngày xuất bản muộn hơn một ngôn ngữ không bắt buộc", + scheduleErrExpireDate1: 'Ngày hết hạn không thể ở trong quá khứ', + scheduleErrExpireDate2: 'Ngày hết hạn không thể trước ngày phát hành', + preventCleanupEnableError: 'Đã xảy ra lỗi khi kích hoạt dọn dẹp phiên bản cho %0%', + preventCleanupDisableError: 'Đã xảy ra lỗi khi vô hiệu hóa dọn dẹp phiên bản cho %0%', + offlineHeadline: 'Offline', + offlineMessage: 'Bạn hiện đang ngoại tuyến. Vui lòng kiểm tra kết nối internet của bạn.', + onlineHeadline: 'Online', + onlineMessage: 'Bạn hiện đang trực tuyến. Bạn có thể tiếp tục làm việc.', + }, + stylesheet: { + addRule: 'Thêm kiểu', + editRule: 'Chỉnh sửa kiểu', + editorRules: 'Các kiểu trình soạn thảo văn bản phong phú', + editorRulesHelp: 'Xác định các kiểu nên có sẵn trong trình soạn thảo văn bản phong phú cho bảng kiểu này', + editstylesheet: 'Chỉnh sửa bảng kiểu', + editstylesheetproperty: 'Chỉnh sửa thuộc tính bảng kiểu', + nameHelp: 'Tên hiển thị trong trình chọn kiểu biên tập', + preview: 'Xem trước', + previewHelp: 'Cách văn bản sẽ xuất hiện trong trình soạn thảo văn bản phong phú.', + selector: 'Selector', + selectorHelp: 'Sử dụng cú pháp CSS, ví dụ: "h1" hoặc ".redHeader"', + styles: 'Các kiểu', + stylesHelp: 'CSS nên được áp dụng trong trình soạn thảo văn bản phong phú, ví dụ: "color:red;"', + tabCode: 'Mã', + tabRules: 'Trình soạn thảo', + }, + template: { + runtimeModeProduction: 'Nội dung không thể chỉnh sửa khi sử dụng chế độ chạy Production.', + deleteByIdFailed: 'Không thể xóa mẫu với ID %0%', + edittemplate: 'Chỉnh sửa mẫu', + insertSections: 'Các phần', + insertContentArea: 'Chèn khu vực nội dung', + insertContentAreaPlaceHolder: 'Chèn giữ chỗ khu vực nội dung', + insert: 'Chèn', + insertDesc: 'Chọn những gì để chèn vào mẫu của bạn', + insertDictionaryItem: 'Mục từ điển', + insertDictionaryItemDesc: + 'Một mục từ điển là một giữ chỗ cho một đoạn văn bản có thể dịch, giúp dễ dàng tạo thiết kế cho các trang web đa ngôn ngữ.', + insertMacro: 'Macro', + insertMacroDesc: + 'Một Macro là một thành phần có thể cấu hình, rất phù hợp cho các phần tái sử dụng trong thiết kế của bạn, nơi bạn cần tùy chọn cung cấp tham số, chẳng hạn như thư viện, biểu mẫu và danh sách.', + insertPageField: 'Giá trị', + insertPageFieldDesc: + 'Hiển thị giá trị của một trường có tên từ trang hiện tại, với tùy chọn để sửa đổi giá trị hoặc quay lại các giá trị thay thế.', + insertPartialView: 'Partial view', + insertPartialViewDesc: + 'Một partial view là một tệp mẫu riêng biệt có thể được kết xuất bên trong một mẫu khác, rất tuyệt vời để tái sử dụng đánh dấu hoặc để tách các mẫu phức tạp thành các tệp riêng biệt.', + mastertemplate: 'Master template', + quickGuide: 'Hướng dẫn nhanh về các thẻ mẫu', + noMaster: 'Không có mẫu chính', + renderBody: 'Kết xuất mẫu con', + renderBodyDesc: 'Kết xuất nội dung của một mẫu con, bằng cách chèn một giữ chỗ @RenderBody().', + defineSection: 'Định nghĩa một phần có tên', + defineSectionDesc: + 'Định nghĩa một phần của mẫu của bạn là một phần có tên bằng cách bao bọc nó trong @section { ... }. Điều này có thể được kết xuất trong một khu vực cụ thể của mẫu cha của mẫu này, bằng cách sử dụng @RenderSection.', + renderSection: 'Kết xuất một phần có tên', + renderSectionDesc: + 'Kết xuất một khu vực được đặt tên của template con, bằng cách chèn một @RenderSection(name). Thao tác này sẽ hiển thị khu vực trong template con được bao bọc bởi một định nghĩa tương ứng @section [name]{ ... }.', + sectionName: 'Tên Section', + sectionMandatory: 'Section là bắt buộc', + sectionMandatoryDesc: + 'Nếu là bắt buộc, template con phải chứa định nghĩa @section, nếu không sẽ hiển thị lỗi.', + queryBuilder: 'Trình dựng truy vấn', + itemsReturned: 'mục được trả về, trong', + publishedItemsReturned: 'Hiện tại có %0% mục đã xuất bản được trả về, trong %1% ms', + iWant: 'Tôi muốn', + allContent: 'tất cả nội dung', + contentOfType: 'nội dung thuộc loại "%0%"', + from: 'từ', + websiteRoot: 'trang web của tôi', + where: 'nơi', + and: 'và', + is: 'là', + isNot: 'không là', + before: 'trước', + beforeIncDate: 'trước (bao gồm ngày đã chọn)', + after: 'sau', + afterIncDate: 'sau (bao gồm ngày đã chọn)', + equals: 'bằng', + doesNotEqual: 'không bằng', + contains: 'chứa', + doesNotContain: 'không chứa', + greaterThan: 'lớn hơn', + greaterThanEqual: 'lớn hơn hoặc bằng', + lessThan: 'nhỏ hơn', + lessThanEqual: 'nhỏ hơn hoặc bằng', + id: 'Id', + name: 'Tên', + createdDate: 'Ngày tạo', + lastUpdatedDate: 'Cập nhật lần cuối', + orderBy: 'sắp xếp theo', + ascending: 'tăng dần', + descending: 'giảm dần', + template: 'Mẫu', + systemFields: 'Trường hệ thống', + }, + grid: { + media: 'Hình ảnh', + macro: 'Macro', + insertControl: 'Chọn loại nội dung', + chooseLayout: 'Chọn một bố cục', + addRows: 'Thêm một hàng', + addElement: 'Thêm nội dung', + dropElement: 'Thả nội dung', + settingsApplied: 'Cài đặt đã được áp dụng', + contentNotAllowed: 'Nội dung này không được phép ở đây', + contentAllowed: 'Nội dung này được phép ở đây', + clickToEmbed: 'Nhấp để nhúng', + clickToInsertImage: 'Nhấp để chèn ảnh', + clickToInsertMacro: 'Nhấp để chèn macro', + placeholderWriteHere: 'Viết ở đây...', + gridLayouts: 'Bố cục lưới', + gridLayoutsDetail: + 'Bố cục là khu vực làm việc tổng thể cho trình chỉnh sửa lưới, thường thì bạn chỉ cần một hoặc hai bố cục khác nhau.', + addGridLayout: 'Thêm bố cục lưới', + editGridLayout: 'Chỉnh sửa bố cục lưới', + addGridLayoutDetail: 'Điều chỉnh bố cục bằng cách thiết lập chiều rộng cột và thêm các phần bổ sung', + rowConfigurations: 'Cấu hình hàng', + rowConfigurationsDetail: 'Các hàng là các ô được định nghĩa trước được sắp xếp theo chiều ngang', + addRowConfiguration: 'Thêm cấu hình hàng', + editRowConfiguration: 'Chỉnh sửa cấu hình hàng', + addRowConfigurationDetail: 'Điều chỉnh hàng bằng cách thiết lập chiều rộng ô và thêm các ô bổ sung', + noConfiguration: 'Không có cấu hình nào khác', + columns: 'Cột', + columnsDetails: 'Tổng số cột kết hợp trong bố cục lưới', + settings: 'Cài đặt', + settingsDetails: 'Cấu hình những gì mà các biên tập viên có thể thay đổi', + styles: 'Styles', + stylesDetails: 'Cấu hình những gì mà các biên tập viên có thể thay đổi về kiểu dáng', + allowAllEditors: 'Cho phép tất cả các biên tập viên', + allowAllRowConfigurations: 'Cho phép tất cả các cấu hình hàng', + maxItems: 'Số mục tối đa', + maxItemsDescription: 'Để trống hoặc đặt thành 0 để không giới hạn', + setAsDefault: 'Đặt làm mặc định', + chooseExtra: 'Chọn thêm', + chooseDefault: 'Chọn mặc định', + areAdded: 'đã được thêm', + warning: 'Cảnh báo', + warningText: + '

Việc thay đổi tên cấu hình hàng sẽ dẫn đến mất dữ liệu cho bất kỳ nội dung nào hiện có dựa trên cấu hình này.

Chỉ thay đổi nhãn sẽ không dẫn đến mất dữ liệu.

', + youAreDeleting: 'Bạn đang xóa cấu hình hàng', + deletingARow: + 'Việc xóa tên cấu hình hàng sẽ dẫn đến mất dữ liệu cho bất kỳ nội dung nào hiện có dựa trên cấu hình này.', + deleteLayout: 'Bạn đang xóa bố cục', + deletingALayout: + 'Việc thay đổi một bố cục sẽ dẫn đến mất dữ liệu cho bất kỳ nội dung nào hiện có dựa trên cấu hình này.', + }, + contentTypeEditor: { + compositions: 'Compositions', + group: 'Nhóm', + groupReorderSameAliasError: + 'Bạn không thể di chuyển nhóm %0% đến tab này vì nhóm sẽ nhận được cùng một bí danh như một tab: "%1%". Đổi tên nhóm để tiếp tục.', + noGroups: 'Bạn chưa thêm bất kỳ nhóm nào', + addGroup: 'Thêm nhóm', + inheritedFrom: 'Kế thừa từ', + addProperty: 'Thêm thuộc tính', + editProperty: 'Chỉnh sửa thuộc tính', + requiredLabel: 'Nhãn bắt buộc', + enableListViewHeading: 'Bật chế độ xem danh sách', + enableListViewDescription: + 'Cấu hình mục nội dung để hiển thị danh sách có thể sắp xếp và tìm kiếm các phần tử con của nó.', + allowedTemplatesHeading: 'Mẫu được phép', + allowedTemplatesDescription: 'Chọn các mẫu mà biên tập viên được phép sử dụng trên nội dung của loại này', + allowAtRootHeading: 'Cho phép ở gốc', + allowAtRootDescription: 'Cho phép biên tập viên tạo nội dung của loại này ở gốc của cây nội dung.', + childNodesHeading: 'Các loại nút con được phép', + childNodesDescription: 'Cho phép nội dung của các loại đã chỉ định được tạo dưới nội dung của loại này.', + chooseChildNode: 'Chọn nút con', + compositionsDescription: + 'Kế thừa các tab và thuộc tính từ một loại tài liệu hiện có. Các tab mới sẽ được thêm vào loại tài liệu hiện tại hoặc được hợp nhất nếu một tab có tên giống hệt tồn tại.', + compositionInUse: 'Loại nội dung này đang được sử dụng trong một thành phần, vì vậy không thể tự tạo thành phần.', + noAvailableCompositions: 'Không có loại nội dung nào có sẵn để sử dụng làm thành phần.', + compositionRemoveWarning: + 'Việc xóa một thành phần sẽ xóa tất cả dữ liệu thuộc tính liên quan. Khi bạn lưu loại tài liệu, sẽ không có cách nào quay lại.', + availableEditors: 'Tạo mới', + reuse: 'Sử dụng hiện có', + editorSettings: 'Cài đặt trình chỉnh sửa', + searchResultSettings: 'Cấu hình có sẵn', + searchResultEditors: 'Tạo một cấu hình mới', + configuration: 'Cấu hình', + yesDelete: 'Có, xóa', + movedUnderneath: 'đã được di chuyển xuống dưới', + copiedUnderneath: 'đã được sao chép xuống dưới', + folderToMove: 'Chọn thư mục để di chuyển', + folderToCopy: 'Chọn thư mục để sao chép', + structureBelow: 'đến cấu trúc cây bên dưới', + allDocumentTypes: 'Tất cả các loại tài liệu', + allDocuments: 'Tất cả tài liệu', + allMediaItems: 'Tất cả mục phương tiện', + usingThisDocument: + 'Việc sử dụng loại tài liệu này sẽ bị xóa vĩnh viễn, vui lòng xác nhận bạn muốn xóa những cái này.', + usingThisMedia: + 'Việc sử dụng loại phương tiện này sẽ bị xóa vĩnh viễn, vui lòng xác nhận bạn muốn xóa những cái này.', + usingThisMember: + 'Việc sử dụng loại thành viên này sẽ bị xóa vĩnh viễn, vui lòng xác nhận bạn muốn xóa những cái này.', + andAllDocuments: 'và tất cả tài liệu sử dụng loại này', + andAllMediaItems: 'và tất cả mục phương tiện sử dụng loại này', + andAllMembers: 'và tất cả thành viên sử dụng loại này', + memberCanEdit: 'Thành viên có thể chỉnh sửa', + memberCanEditDescription: 'Cho phép giá trị thuộc tính này được chỉnh sửa bởi thành viên trên trang hồ sơ của họ', + isSensitiveData: 'Là dữ liệu nhạy cảm', + isSensitiveDataDescription: + 'Ẩn giá trị thuộc tính này khỏi các biên tập viên nội dung không có quyền truy cập để xem thông tin nhạy cảm', + showOnMemberProfile: 'Hiển thị trên hồ sơ thành viên', + showOnMemberProfileDescription: 'Cho phép giá trị thuộc tính này được hiển thị trên trang hồ sơ thành viên', + tabHasNoSortOrder: 'tab không có thứ tự sắp xếp', + compositionUsageHeading: 'Loại tài liệu này đang được sử dụng ở đâu?', + compositionUsageSpecification: + 'Loại tài liệu này hiện đang được sử dụng trong thành phần của các loại nội dung sau:', + variantsHeading: 'Biến thể', + cultureVariantHeading: 'Cho phép thay đổi theo văn hóa', + segmentVariantHeading: 'Cho phép phân đoạn', + cultureInvariantLabel: 'Chia sẻ giữa các nền văn hóa', + segmentInvariantLabel: 'Chia sẻ giữa các phân đoạn', + cultureAndVariantInvariantLabel: 'Chia sẻ giữa các nền văn hóa và phân đoạn', + cultureVariantLabel: 'Thay đổi theo văn hóa', + segmentVariantLabel: 'Thay đổi theo phân đoạn', + variantsDescription: 'Cho phép biên tập viên tạo nội dung của loại này bằng nhiều ngôn ngữ khác nhau.', + cultureVariantDescription: 'Cho phép biên tập viên tạo nội dung bằng các ngôn ngữ khác nhau.', + segmentVariantDescription: 'Cho phép biên tập viên tạo các phân đoạn của nội dung này.', + allowVaryByCulture: 'Cho phép thay đổi theo văn hóa', + allowVaryBySegment: 'Cho phép phân đoạn', + elementType: 'Loại phần tử', + elementHeading: 'Là loại phần tử', + elementDescription: 'Một loại phần tử được sử dụng trong các loại tài liệu khác, và không nằm trong cây nội dung.', + elementCannotToggle: + 'Loại tài liệu không thể được thay đổi thành loại phần tử một khi nó đã được sử dụng để tạo một hoặc nhiều mục nội dung.', + elementDoesNotSupport: 'Điều này không áp dụng cho loại phần tử', + propertyHasChanges: 'Bạn đã thực hiện thay đổi đối với thuộc tính này. Bạn có chắc chắn muốn bỏ qua chúng không?', + displaySettingsHeadline: 'Hình thức', + displaySettingsLabelOnLeft: 'Nhãn ở bên trái', + displaySettingsLabelOnTop: 'Nhãn ở trên (đầy đủ chiều rộng)', + confirmDeleteTabMessage: 'Bạn có chắc chắn muốn xóa tab %0% không?', + confirmDeleteGroupMessage: 'Bạn có chắc chắn muốn xóa nhóm %0% không?', + confirmDeletePropertyMessage: 'Bạn có chắc chắn muốn xóa thuộc tính %0% không?', + confirmDeleteTabNotice: 'Điều này cũng sẽ xóa tất cả các mục bên dưới tab này.', + confirmDeleteGroupNotice: 'Điều này cũng sẽ xóa tất cả các mục bên dưới nhóm này.', + addTab: 'Thêm tab', + convertToTab: 'Chuyển đổi thành tab', + tabDirectPropertiesDropZone: 'Kéo thuộc tính vào đây để đặt trực tiếp trên tab', + removeChildNode: 'Bạn đang xóa nút con', + removeChildNodeWarning: + 'Việc xóa một nút con sẽ hạn chế các tùy chọn của biên tập viên để tạo các loại nội dung khác nhau bên dưới một nút.', + usingEditor: 'sử dụng trình chỉnh sửa này sẽ được cập nhật với các cài đặt mới.', + historyCleanupHeading: 'Làm sạch lịch sử', + historyCleanupDescription: 'Cho phép ghi đè các cài đặt làm sạch lịch sử toàn cầu.', + historyCleanupKeepAllVersionsNewerThanDays: 'Giữ tất cả các phiên bản mới hơn ngày', + historyCleanupKeepLatestVersionPerDayForDays: 'Giữ phiên bản mới nhất mỗi ngày trong nhiều ngày', + historyCleanupPreventCleanup: 'Ngăn chặn việc làm sạch', + historyCleanupEnableCleanup: 'Bật làm sạch', + historyCleanupGloballyDisabled: + 'LƯU Ý! Việc dọn dẹp các phiên bản nội dung cũ hiện đang bị tắt toàn cục. Các thiết lập này sẽ không có hiệu lực cho đến khi được bật.', + changeDataTypeHelpText: + 'Việc thay đổi kiểu dữ liệu có giá trị đã lưu bị tắt. Để cho phép, bạn có thể thay đổi thiết lập Umbraco:CMS:DataTypes:CanBeChanged trong file appsettings.json.', + collection: 'Collection', + collectionDescription: 'Cấu hình tổng quan về nội dung con.', + collections: 'Collections', + collectionsDescription: 'Cấu hình mục nội dung để hiển thị danh sách các mục con của nó.', + structure: 'Cấu trúc', + presentation: 'Trình bày', + }, + webhooks: { + addWebhook: 'Tạo webhook', + addWebhookHeader: 'Thêm tiêu đề webhook', + addDocumentType: 'Thêm loại tài liệu', + addMediaType: 'Thêm loại phương tiện', + createHeader: 'Tạo tiêu đề', + deliveries: 'Giao hàng', + noHeaders: 'Chưa có tiêu đề webhook nào được thêm', + noEventsFound: 'Không tìm thấy sự kiện nào.', + enabled: 'Đã bật', + disabled: 'Đã tắt', + events: 'Sự kiện', + event: 'Sự kiện', + url: 'URL', + types: 'Loại nội dung', + webhookKey: 'Khóa webhook', + retryCount: 'Số lần thử lại', + urlDescription: 'URL để gọi khi webhook được kích hoạt.', + eventDescription: 'Các sự kiện mà webhook nên được kích hoạt.', + contentTypeDescription: 'Chỉ kích hoạt webhook cho một loại nội dung cụ thể.', + enabledDescription: 'Webhook có được bật không?', + headersDescription: 'Các tiêu đề tùy chỉnh để bao gồm trong yêu cầu webhook.', + contentType: 'Loại nội dung', + headers: 'Tiêu đề', + selectEventFirst: 'Vui lòng chọn một sự kiện trước.', + selectEvents: 'Chọn sự kiện', + statusCode: 'Mã trạng thái', + unnamedWebhook: 'Webhook không tên', + }, + languages: { + addLanguage: 'Thêm ngôn ngữ', + culture: 'Mã ISO', + mandatoryLanguage: 'Ngôn ngữ bắt buộc', + mandatoryLanguageHelp: 'Các thuộc tính trên ngôn ngữ này phải được điền trước khi nút có thể được xuất bản.', + defaultLanguage: 'Ngôn ngữ mặc định', + defaultLanguageHelp: 'Một trang Umbraco chỉ có thể có một ngôn ngữ mặc định được thiết lập.', + changingDefaultLanguageWarning: 'Chuyển đổi ngôn ngữ mặc định có thể dẫn đến việc thiếu nội dung mặc định.', + fallsbackToLabel: 'Quay về', + noFallbackLanguageOption: 'Không có ngôn ngữ dự phòng', + fallbackLanguageDescription: + 'Để cho phép nội dung đa ngôn ngữ quay về một ngôn ngữ khác nếu không có trong ngôn ngữ yêu cầu, hãy chọn nó ở đây.', + fallbackLanguage: 'Ngôn ngữ dự phòng', + none: 'không có', + invariantPropertyUnlockHelp: '%0% được chia sẻ giữa các ngôn ngữ và phân đoạn.', + invariantCulturePropertyUnlockHelp: '%0% được chia sẻ giữa tất cả các ngôn ngữ.', + invariantSegmentPropertyUnlockHelp: '%0% được chia sẻ giữa tất cả các phân đoạn.', + invariantLanguageProperty: 'Chia sẻ: Ngôn ngữ', + invariantSegmentProperty: 'Chia sẻ: Phân đoạn', + }, + macro: { + addParameter: 'Thêm tham số', + editParameter: 'Chỉnh sửa tham số', + enterMacroName: 'Nhập tên macro', + parameters: 'Tham số', + parametersDescription: 'Định nghĩa các tham số mà nên có sẵn khi sử dụng macro này.', + selectViewFile: 'Chọn tệp macro view một phần', + }, + modelsBuilder: { + buildingModels: 'Đang xây dựng mô hình', + waitingMessage: 'Điều này có thể mất một chút thời gian, đừng lo lắng', + modelsGenerated: 'Mô hình đã được tạo', + modelsGeneratedError: 'Không thể tạo mô hình', + modelsExceptionInUlog: 'Quá trình tạo mô hình đã thất bại, xem ngoại lệ trong nhật ký U', + }, + templateEditor: { + addDefaultValue: 'Thêm giá trị mặc định', + defaultValue: 'Giá trị mặc định', + alternativeField: 'Trường dự phòng', + alternativeText: 'Giá trị mặc định', + casing: 'Casing', + encoding: 'Mã hóa', + chooseField: 'Chọn trường', + convertLineBreaks: 'Chuyển đổi ngắt dòng', + convertLineBreaksHelp: "Thay thế ngắt dòng bằng thẻ html 'br'", + customFields: 'Trường tùy chỉnh', + dateOnly: 'Chỉ ngày', + formatAsDate: 'Định dạng dưới dạng ngày', + htmlEncode: 'Mã hóa HTML', + htmlEncodeHelp: 'Sẽ thay thế các ký tự đặc biệt bằng tương đương HTML của chúng.', + insertedAfter: 'Sẽ được chèn sau giá trị trường', + insertedBefore: 'Sẽ được chèn trước giá trị trường', + lowercase: 'Chữ thường', + none: 'Không có', + outputSample: 'Mẫu đầu ra', + postContent: 'Chèn sau trường', + preContent: 'Chèn trước trường', + recursive: 'Đệ quy', + recursiveDescr: 'Có, làm cho nó đệ quy', + removeParagraph: 'Xóa thẻ đoạn', + removeParagraphHelp: 'Sẽ xóa các thẻ đoạn khỏi giá trị trường', + standardFields: 'Trường tiêu chuẩn', + uppercase: 'Chữ hoa', + urlEncode: 'Mã hóa URL', + urlEncodeHelp: 'Sẽ định dạng các ký tự đặc biệt trong URL', + usedIfAllEmpty: 'Sẽ chỉ được sử dụng khi các giá trị trường ở trên đều trống', + usedIfEmpty: 'Trường này sẽ chỉ được sử dụng nếu trường chính trống', + withTime: 'Ngày và giờ', + }, + translation: { + details: 'Chi tiết dịch thuật', + DownloadXmlDTD: 'Tải xuống XML DTD', + fields: 'Các trường', + includeSubpages: 'Bao gồm các trang con', + noTranslators: + 'Không tìm thấy người dùng dịch thuật. Vui lòng tạo một người dùng dịch thuật trước khi bạn bắt đầu gửi nội dung để dịch', + pageHasBeenSendToTranslation: "Trang '%0%' đã được gửi để dịch", + sendToTranslate: "Gửi trang '%0%' để dịch", + totalWords: 'Tổng số từ', + translateTo: 'Dịch sang', + translationDone: 'Dịch thuật đã hoàn thành.', + translationDoneHelp: + 'Bạn có thể xem trước các trang mà bạn vừa dịch bằng cách nhấp vào bên dưới. Nếu trang gốc được tìm thấy, bạn sẽ nhận được sự so sánh của 2 trang.', + translationFailed: 'Dịch thuật không thành công, tệp XML có thể bị hỏng', + translationOptions: 'Tùy chọn dịch thuật', + translator: 'Người dịch', + uploadTranslationXml: 'Tải lên XML dịch thuật', + }, + treeHeaders: { + content: 'Nội dung', + contentBlueprints: 'Mẫu tài liệu', + media: 'Phương tiện', + cacheBrowser: 'Bộ nhớ đệm trình duyệt', + contentRecycleBin: 'Thùng rác', + createdPackages: 'Gói đã tạo', + dataTypes: 'Kiểu dữ liệu', + dictionary: 'Từ điển', + installedPackages: 'Gói đã cài đặt', + installSkin: 'Cài đặt giao diện', + installStarterKit: 'Cài đặt bộ khởi động', + languages: 'Ngôn ngữ', + localPackage: 'Cài đặt gói cục bộ', + macros: 'Macros', + mediaTypes: 'Loại Phương tiện', + member: 'Thành viên', + memberGroups: 'Nhóm thành viên', + memberRoles: 'Vai trò thành viên', + memberTypes: 'Loại thành viên', + documentTypes: 'Loại tài liệu', + relationTypes: 'Loại quan hệ', + packager: 'Gói mở rộng', + packages: 'Gói mở rộng', + partialViews: 'Partial Views', + partialViewMacros: 'Partial View Macro Files', + repositories: 'Cài đặt từ kho lưu trữ', + relations: 'Relations', + runway: 'Cài đặt Runway', + runwayModules: 'Mô-đun Runway', + scripting: 'Tệp Scripting', + scripts: 'Tệp Script', + stylesheets: 'Tệp Stylesheet', + templates: 'Tệp Template', + logViewer: 'Xem nhật ký', + userPermissions: 'Quyền người dùng', + userTypes: 'Loại người dùng', + users: 'Người dùng', + settingsGroup: 'Cài đặt', + templatingGroup: 'Hệ thống mẫu', + thirdPartyGroup: 'Bên thứ ba', + structureGroup: 'Cấu trúc', + advancedGroup: 'Nâng cao', + webhooks: 'Webhooks', + }, + update: { + updateAvailable: 'Cập nhật mới sẵn sàng', + updateDownloadText: '%0% đã sẵn sàng, nhấp vào đây để tải xuống', + updateNoServer: 'Không có kết nối đến máy chủ', + updateNoServerError: 'Lỗi khi kiểm tra cập nhật. Vui lòng xem lại trace-stack để biết thêm thông tin', + }, + user: { + access: 'Quyền truy cập', + accessHelp: 'Dựa trên các nhóm và nút khởi động được gán, người dùng có quyền truy cập đến các nút sau', + assignAccess: 'Gán quyền truy cập', + administrators: 'Quản trị viên', + categoryField: 'Trường danh mục', + createDate: 'Ngày tạo người dùng', + createUserHeadline: (kind: string) => { + return kind === 'Api' ? 'Tạo người dùng API' : 'Tạo người dùng'; + }, + createUserDescription: (kind: string) => { + const defaultUserText = `Tạo một người dùng để cấp quyền truy cập cho họ vào Umbraco. Khi một người dùng được tạo, một mật khẩu sẽ được tạo ra mà bạn có thể chia sẻ với họ.`; + const apiUserText = `Tạo một người dùng API để cho phép các dịch vụ bên ngoài xác thực với Umbraco Management API.`; + return kind === 'Api' ? apiUserText : defaultUserText; + }, + changePassword: 'Đổi mật khẩu', + changePhoto: 'Đổi ảnh', + configureMfa: 'Cấu hình MFA', + emailRequired: 'Bắt buộc - nhập địa chỉ email cho người dùng này', + emailDescription: (usernameIsEmail: boolean) => { + return usernameIsEmail + ? 'Địa chỉ email được dùng cho thông báo, khôi phục mật khẩu và làm tên đăng nhập' + : 'Địa chỉ email được dùng cho thông báo và khôi phục mật khẩu'; + }, + kind: 'Loại', + newPassword: 'Mật khẩu mới', + newPasswordFormatLengthTip: 'Còn lại tối thiểu %0% ký tự!', + newPasswordFormatNonAlphaTip: 'Phải có ít nhất %0% ký tự đặc biệt.', + noLockouts: 'không bị khóa', + noPasswordChange: 'Mật khẩu chưa được thay đổi', + confirmNewPassword: 'Xác nhận mật khẩu mới', + changePasswordDescription: + "Bạn có thể thay đổi mật khẩu của mình để truy cập vào Umbraco backoffice bằng cách điền vào mẫu dưới đây và nhấp vào nút 'Đổi mật khẩu'", + contentChannel: 'Kênh nội dung', + createAnotherUser: 'Tạo người dùng khác', + createUserHelp: + 'Tạo người dùng mới để cấp quyền truy cập cho họ vào Umbraco. Khi một người dùng mới được tạo, một mật khẩu sẽ được tạo ra mà bạn có thể chia sẻ với người dùng đó.', + descriptionField: 'Trường mô tả', + disabled: 'Vô hiệu hóa người dùng', + documentType: 'Loại tài liệu', + duplicateLogin: 'Người dùng với tên đăng nhập này đã tồn tại', + editors: 'Biên tập viên', + excerptField: 'Trường trích đoạn', + failedPasswordAttempts: 'Số lần đăng nhập thất bại', + goToProfile: 'Đi đến hồ sơ người dùng', + groupsHelp: 'Thêm nhóm để cấp quyền truy cập và phân quyền', + invite: 'Mời', + inviteAnotherUser: 'Mời người dùng khác', + inviteUserHelp: + 'Mời người dùng mới để cấp quyền truy cập cho họ vào Umbraco. Một email mời sẽ được gửi đến người dùng với thông tin về cách đăng nhập vào Umbraco. Thời gian hiệu lực của lời mời là 72 giờ.', + language: 'Ngôn ngữ giao diện', + languageHelp: 'Thiết lập ngôn ngữ bạn sẽ thấy trong menu và hộp thoại', + languageNotFound: (culture: string, baseCulture: string) => + `Culture đã chỉ định "${culture}" không được tìm thấy, culture gốc "${baseCulture}" sẽ được sử dụng.`, + languageNotFoundFallback: (culture: string, baseCulture: string) => + `Không tìm thấy culture đã chỉ định "${culture}" hoặc culture gốc "${baseCulture}", nên hệ thống sẽ sử dụng culture mặc định thay thế là "English (UK)".`, + lastLockoutDate: 'Ngày bị khóa lần cuối', + lastLogin: 'Đăng nhập lần cuối', + lastPasswordChangeDate: 'Mật khẩu thay đổi lần cuối', + loginname: 'Tên đăng nhập', + loginnameRequired: 'Bắt buộc - nhập tên đăng nhập cho người dùng này', + loginnameDescription: 'Tên đăng nhập được sử dụng để đăng nhập', + mediastartnode: 'Nút bắt đầu media', + mediastartnodehelp: 'Giới hạn thư viện media đến nút bắt đầu cụ thể', + mediastartnodes: 'Media start nodes', + mediastartnodeshelp: 'Giới hạn thư viện media đến các nút bắt đầu cụ thể', + modules: 'Sections', + nameRequired: 'Bắt buộc - nhập tên cho người dùng này', + noConsole: 'Vô hiệu hóa quyền truy cập Umbraco', + noLogin: 'chưa đăng nhập', + oldPassword: 'Mật khẩu cũ', + password: 'Mật khẩu', + resetPassword: 'Đặt lại mật khẩu', + passwordChanged: 'Mật khẩu của bạn đã được thay đổi!', + passwordChangedGeneric: 'Mật khẩu đã được thay đổi', + passwordConfirm: 'Vui lòng xác nhận mật khẩu mới', + passwordEnterNew: 'Nhập mật khẩu mới của bạn', + passwordIsBlank: 'Mật khẩu mới của bạn không được để trống!', + passwordCurrent: 'Mật khẩu hiện tại', + passwordInvalid: 'Mật khẩu hiện tại không hợp lệ', + passwordIsDifferent: 'Mật khẩu mới và mật khẩu xác nhận không khớp. Vui lòng thử lại!', + passwordMismatch: 'Mật khẩu xác nhận không khớp với mật khẩu mới!', + passwordRequiresDigit: "Mật khẩu phải có ít nhất một chữ số ('0'-'9')", + passwordRequiresLower: "Mật khẩu phải có ít nhất một chữ cái thường ('a'-'z')", + passwordRequiresNonAlphanumeric: 'Mật khẩu phải có ít nhất một ký tự không phải chữ cái hoặc số', + passwordRequiresUniqueChars: 'Mật khẩu phải sử dụng ít nhất %0% ký tự khác nhau', + passwordRequiresUpper: "Mật khẩu phải có ít nhất một chữ cái hoa ('A'-'Z')", + passwordTooShort: 'Mật khẩu phải có ít nhất %0% ký tự', + permissionReplaceChildren: 'Thay thế quyền truy cập của các nút con', + permissionSelectedPages: 'Bạn đang sửa đổi quyền truy cập cho các trang:', + permissionSelectPages: 'Chọn các trang để sửa đổi quyền truy cập của chúng', + removePhoto: 'Xóa ảnh', + permissionsDefault: 'Quyền mặc định', + permissionsGranular: 'Quyền chi tiết', + permissionsGranularHelp: 'Thiết lập quyền cho các nút cụ thể', + granularRightsLabel: 'Tài liệu', + granularRightsDescription: 'Gán quyền cho các tài liệu cụ thể', + permissionsEntityGroup_document: 'Tài liệu', + permissionsEntityGroup_media: 'Phương tiện', + permissionsEntityGroup_member: 'Thành viên', + 'permissionsEntityGroup_document-property-value': 'Giá trị thuộc tính tài liệu', + permissionNoVerbs: 'Không có quyền cho phép', + profile: 'Hồ sơ', + searchAllChildren: 'Tìm kiếm tất cả các nút con', + languagesHelp: 'Giới hạn các ngôn ngữ mà người dùng có quyền chỉnh sửa', + allowAccessToAllLanguages: 'Cho phép truy cập tất cả ngôn ngữ', + allowAccessToAllDocuments: 'Cho phép truy cập tất cả tài liệu', + allowAccessToAllMedia: 'Cho phép truy cập tất cả media', + sectionsHelp: 'Thêm các phần để cấp quyền truy cập cho người dùng', + selectUserGroup: (multiple: boolean) => { + return multiple ? 'Chọn nhóm người dùng' : 'Chọn nhóm người dùng'; + }, + chooseUserGroup: (multiple: boolean) => { + return multiple ? 'Chọn nhóm người dùng' : 'Chọn nhóm người dùng'; + }, + entityPermissionsLabel: 'Quyền hạn', + entityPermissionsDescription: 'Gán quyền cho các thao tác', + noStartNode: 'Chưa chọn nút bắt đầu', + noStartNodes: 'Chưa chọn các nút bắt đầu', + startnode: 'Nút bắt đầu nội dung', + startnodehelp: 'Giới hạn cây nội dung đến một nút bắt đầu cụ thể', + startnodes: 'Nút bắt đầu nội dung', + startnodeshelp: 'Giới hạn cây nội dung đến các nút bắt đầu cụ thể', + updateDate: 'Ngày cập nhật người dùng lần cuối', + userCreated: 'đã được tạo', + userCreatedSuccessHelp: + 'Người dùng mới đã được tạo thành công. Để đăng nhập vào Umbraco, hãy sử dụng mật khẩu bên dưới.', + userHasPassword: 'Người dùng đã có mật khẩu được đặt', + userHasGroup: "Người dùng đã ở trong nhóm '%0%'", + userLockoutNotEnabled: 'Tính năng khóa tài khoản không được bật cho người dùng này', + userManagement: 'Quản lý người dùng', + username: 'Tên', + userNotInGroup: "Người dùng không ở trong nhóm '%0%'", + userPermissions: 'Quyền của người dùng', + usergroup: 'Nhóm người dùng', + usergroups: 'Nhóm người dùng', + userInvited: 'đã được mời', + userInvitedSuccessHelp: + 'Một lời mời đã được gửi đến người dùng mới với thông tin chi tiết về cách đăng nhập vào Umbraco.', + userinviteWelcomeMessage: + 'Xin chào và chào mừng bạn đến với Umbraco! Chỉ trong 1 phút, bạn sẽ sẵn sàng sử dụng. Chúng tôi chỉ cần bạn tạo mật khẩu và thêm hình đại diện cho tài khoản.', + userinviteExpiredMessage: + 'Chào mừng bạn đến với Umbraco! Rất tiếc, lời mời của bạn đã hết hạn. Vui lòng liên hệ với quản trị viên của bạn và yêu cầu họ gửi lại lời mời.', + userinviteAvatarMessage: + 'Việc tải lên một bức ảnh của bạn sẽ giúp những người dùng khác nhận ra bạn dễ dàng hơn. Nhấp vào hình tròn ở trên để tải lên ảnh của bạn.', + writer: 'Người viết', + configureTwoFactor: 'Cấu hình xác thực hai yếu tố', + change: 'Thay đổi', + yourProfile: 'Hồ sơ của bạn', + yourHistory: 'Lịch sử gần đây của bạn', + sessionExpires: 'Phiên làm việc sẽ hết hạn trong', + inviteUser: 'Mời người dùng', + createUser: 'Tạo người dùng', + sendInvite: 'Gửi lời mời', + backToUsers: 'Quay lại người dùng', + defaultInvitationMessage: 'Gửi lại lời mời...', + deleteUser: 'Xóa người dùng', + deleteUserConfirmation: 'Bạn có chắc chắn muốn xóa tài khoản người dùng này không?', + stateAll: 'Tất cả', + stateActive: 'Đang hoạt động', + stateDisabled: 'Đã tắt', + stateLockedOut: 'Bị khóa', + stateApproved: 'Đã phê duyệt', + stateInvited: 'Đã mời', + stateInactive: 'Không hoạt động', + sortNameAscending: 'Tên (A-Z)', + sortNameDescending: 'Tên (Z-A)', + sortCreateDateDescending: 'Mới nhất', + sortCreateDateAscending: 'Cũ nhất', + sortLastLoginDateDescending: 'Lần đăng nhập cuối', + userKindDefault: 'Người dùng', + userKindApi: 'Người dùng API', + noUserGroupsAdded: 'Chưa có nhóm người dùng nào được thêm', + '2faDisableText': + 'Nếu bạn muốn vô hiệu hóa nhà cung cấp xác thực hai yếu tố này, bạn phải nhập mã hiển thị trên thiết bị xác thực của mình:', + '2faProviderIsEnabled': 'Nhà cung cấp xác thực hai yếu tố này đã được bật', + '2faProviderIsEnabledMsg': '{0} hiện đã được bật', + '2faProviderIsNotEnabledMsg': 'Đã xảy ra sự cố khi cố gắng bật {0}', + '2faProviderIsDisabledMsg': '{0} hiện đã bị vô hiệu hóa', + '2faProviderIsNotDisabledMsg': 'Đã xảy ra sự cố khi cố gắng vô hiệu hóa {0}', + '2faDisableForUser': 'Bạn có muốn vô hiệu hóa "{0}" cho người dùng này không?', + '2faQrCodeAlt': 'QR code cho xác thực hai yếu tố với {0}', + '2faQrCodeTitle': 'QR code cho xác thực hai yếu tố với {0}', + '2faQrCodeDescription': 'Quét mã QR này bằng ứng dụng xác thực của bạn để bật xác thực hai yếu tố', + '2faCodeInput': 'Mã xác minh', + '2faCodeInputHelp': 'Vui lòng nhập mã xác minh', + '2faInvalidCode': 'Mã không hợp lệ đã được nhập', + }, + validation: { + validation: 'Xác thực', + validateNothing: 'Không xác thực', + validateAsEmail: 'Xác thực dưới dạng địa chỉ email', + validateAsNumber: 'Xác thực dưới dạng số', + validateAsUrl: 'Xác thực dưới dạng URL', + enterCustomValidation: '...hoặc nhập xác thực tùy chỉnh', + fieldIsMandatory: 'Trường là bắt buộc', + mandatoryMessage: 'Nhập thông báo lỗi xác thực tùy chỉnh (tùy chọn)', + validationRegExp: 'Nhập biểu thức chính quy', + validationRegExpMessage: 'Nhập thông báo lỗi xác thực tùy chỉnh (tùy chọn)', + minCount: 'Bạn cần thêm ít nhất', + maxCount: 'Bạn chỉ có thể có', + addUpTo: 'Thêm tối đa', + items: 'mục', + urls: 'URL(s)', + urlsSelected: 'URL(s) đã chọn', + itemsSelected: 'mục(s) đã chọn', + invalidDate: 'Ngày không hợp lệ', + invalidNumber: 'Không phải là số', + invalidNumberStepSize: 'Không phải là kích thước bước số hợp lệ', + invalidEmail: 'Email không hợp lệ', + invalidNull: 'Giá trị không được để trống', + invalidEmpty: 'Giá trị không được để trống', + invalidFalse: 'Trường này phải được bật', + invalidPattern: 'Giá trị không hợp lệ, nó không khớp với mẫu chính xác', + customValidation: 'Xác thực tùy chỉnh', + entriesShort: 'Tối thiểu %0% mục, cần thêm %1% mục nữa.', + entriesExceed: 'Tối đa %0% mục, bạn đã nhập %1% mục quá nhiều.', + entriesAreasMismatch: 'Yêu cầu về số lượng nội dung chưa được đáp ứng cho một hoặc nhiều khu vực', + invalidMemberGroupName: 'Tên nhóm thành viên không hợp lệ', + invalidUserGroupName: 'Tên nhóm người dùng không hợp lệ', + invalidToken: 'Mã thông báo không hợp lệ', + invalidUsername: 'Tên người dùng không hợp lệ', + duplicateEmail: "Email '%0%' đã được sử dụng", + duplicateUserGroupName: "Tên nhóm người dùng '%0%' đã tồn tại.", + duplicateMemberGroupName: "Tên nhóm thành viên '%0%' đã tồn tại.", + duplicateUsername: "Tên người dùng '%0%' đã tồn tại.", + legacyOption: 'Tùy chọn cũ', + legacyOptionDescription: 'Tùy chọn này không còn được hỗ trợ, vui lòng chọn tùy chọn khác', + numberMinimum: "Giá trị phải lớn hơn hoặc bằng '%0%'.", + numberMaximum: "Giá trị phải nhỏ hơn hoặc bằng '%0%'.", + numberMisconfigured: "Giá trị tối thiểu '%0%' phải nhỏ hơn giá trị tối đa '%1%'.", + invalidExtensions: 'Một hoặc nhiều phần mở rộng không hợp lệ.', + allowedExtensions: 'Các phần mở rộng được phép là:', + disallowedExtensions: 'Các phần mở rộng không được phép là:', + }, + healthcheck: { + checkSuccessMessage: "Giá trị được đặt thành giá trị được khuyến nghị: '%0%'.", + checkErrorMessageDifferentExpectedValue: + "Giá trị mong đợi '%1%' cho '%2%' trong tệp cấu hình '%3%', nhưng tìm thấy '%0%'.", + checkErrorMessageUnexpectedValue: "Tìm thấy giá trị không mong đợi '%0%' cho '%2%' trong tệp cấu hình '%3%'.", + macroErrorModeCheckSuccessMessage: "MacroErrors được đặt thành '%0%'.", + macroErrorModeCheckErrorMessage: + "MacroErrors được đặt thành '%0%' sẽ ngăn một hoặc tất cả các trang trên trang web của bạn tải hoàn toàn nếu có bất kỳ lỗi nào trong các macro. Việc khắc phục điều này sẽ đặt giá trị thành '%1%'.", + httpsCheckValidCertificate: 'Chứng chỉ của trang web của bạn là hợp lệ.', + httpsCheckInvalidCertificate: "Lỗi xác thực chứng chỉ: '%0%'", + httpsCheckExpiredCertificate: 'Chứng chỉ SSL của trang web của bạn đã hết hạn.', + httpsCheckExpiringCertificate: 'Chứng chỉ SSL của trang web của bạn sẽ hết hạn trong %0% ngày.', + healthCheckInvalidUrl: "Lỗi khi ping URL %0% - '%1%'", + httpsCheckIsCurrentSchemeHttps: 'Hiện tại bạn đang %0% xem trang web bằng giao thức HTTPS.', + httpsCheckConfigurationRectifyNotPossible: + "Giá trị 'Umbraco:CMS:Global:UseHttps' trong tệp appSettings.json của bạn được đặt thành 'false'. Khi bạn truy cập trang này bằng giao thức HTTPS, giá trị này nên được đặt thành 'true'.", + httpsCheckConfigurationCheckResult: + "Giá trị 'Umbraco:CMS:Global:UseHttps' trong tệp appSettings.json của bạn được đặt thành '%0%', cookie của bạn được đánh dấu là %1% an toàn.", + compilationDebugCheckSuccessMessage: 'Chế độ biên dịch gỡ lỗi đã bị vô hiệu hóa.', + compilationDebugCheckErrorMessage: + 'Chế độ biên dịch gỡ lỗi hiện đang được bật. Nên vô hiệu hóa cài đặt này trước khi đưa vào sử dụng.', + umbracoApplicationUrlCheckResultTrue: + "Giá trị 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' được đặt thành %0%.", + umbracoApplicationUrlCheckResultFalse: "Giá trị 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' không được đặt.", + clickJackingCheckHeaderFound: + 'Đã tìm thấy tiêu đề hoặc thẻ meta X-Frame-Options được sử dụng để kiểm soát xem một trang có thể được IFRAME bởi trang khác hay không.', + clickJackingCheckHeaderNotFound: + 'Không tìm thấy tiêu đề hoặc thẻ meta X-Frame-Options được sử dụng để kiểm soát xem một trang có thể được IFRAME bởi trang khác hay không.', + noSniffCheckHeaderFound: + 'Đã tìm thấy tiêu đề hoặc thẻ meta X-Content-Type-Options được sử dụng để bảo vệ chống lại các lỗ hổng MIME sniffing.', + noSniffCheckHeaderNotFound: + 'Không tìm thấy tiêu đề hoặc thẻ meta X-Content-Type-Options được sử dụng để bảo vệ chống lại các lỗ hổng MIME sniffing.', + hSTSCheckHeaderFound: + 'Đã tìm thấy tiêu đề Strict-Transport-Security, còn được gọi là tiêu đề HSTS.', + hSTSCheckHeaderNotFound: 'Không tìm thấy tiêu đề Strict-Transport-Security.', + hSTSCheckHeaderFoundOnLocalhost: + 'Đã tìm thấy tiêu đề Strict-Transport-Security, còn được gọi là tiêu đề HSTS. Tiêu đề này không nên xuất hiện trên localhost.', + hSTSCheckHeaderNotFoundOnLocalhost: + 'Không tìm thấy tiêu đề Strict-Transport-Security. Tiêu đề này không nên xuất hiện trên localhost.', + xssProtectionCheckHeaderFound: + 'Đã tìm thấy tiêu đề X-XSS-Protection. Không nên thêm tiêu đề này vào trang web của bạn.
Bạn có thể đọc về điều này trên trang web Mozilla', + xssProtectionCheckHeaderNotFound: 'Không tìm thấy tiêu đề X-XSS-Protection.', + excessiveHeadersFound: + 'Đã tìm thấy các tiêu đề sau đây tiết lộ thông tin về công nghệ trang web: %0%.', + excessiveHeadersNotFound: 'Không tìm thấy tiêu đề nào tiết lộ thông tin về công nghệ trang web.', + smtpMailSettingsNotFound: "Không tìm thấy cấu hình 'Umbraco:CMS:Global:Smtp'.", + smtpMailSettingsHostNotConfigured: "Không tìm thấy cấu hình 'Umbraco:CMS:Global:Smtp:Host'.", + smtpMailSettingsConnectionSuccess: + 'Cấu hình SMTP đã được thiết lập chính xác và dịch vụ đang hoạt động như mong đợi.', + smtpMailSettingsConnectionFail: + "Không thể kết nối đến máy chủ SMTP được cấu hình với host '%0%' và port '%1%'. Vui lòng kiểm tra để đảm bảo rằng các cài đặt SMTP trong cấu hình 'Umbraco:CMS:Global:Smtp' là chính xác.", + notificationEmailsCheckSuccessMessage: 'Email thông báo đã được thiết lập thành %0%.', + notificationEmailsCheckErrorMessage: + 'Email thông báo vẫn được thiết lập thành giá trị mặc định là %0%.', + checkGroup: 'Nhóm kiểm tra', + helpText: + '

Trình kiểm tra sức khỏe đánh giá nhiều lĩnh vực của trang web của bạn để tìm các cài đặt, cấu hình, vấn đề tiềm ẩn, v.v. tốt nhất. Bạn có thể dễ dàng khắc phục sự cố bằng cách nhấn một nút. Bạn có thể thêm các kiểm tra sức khỏe của riêng mình, hãy xem tài liệu để biết thêm thông tin về các kiểm tra sức khỏe tùy chỉnh.

', + }, + redirectUrls: { + disableUrlTracker: 'Tắt theo dõi URL', + enableUrlTracker: 'Bật theo dõi URL', + originalUrl: 'URL gốc', + redirectedTo: 'Chuyển hướng đến', + redirectUrlManagement: 'Quản lý URL chuyển hướng', + panelInformation: 'Các URL sau đây chuyển hướng đến mục nội dung này:', + noRedirects: 'Không có chuyển hướng nào đã được thực hiện', + noRedirectsDescription: + 'Khi một trang đã xuất bản được đổi tên hoặc di chuyển, một chuyển hướng sẽ tự động được tạo đến trang mới.', + redirectRemoved: 'Đã xóa URL chuyển hướng.', + redirectRemoveError: 'Lỗi khi xóa URL chuyển hướng.', + redirectRemoveWarning: 'Điều này sẽ xóa chuyển hướng', + confirmDisable: 'Bạn có chắc chắn muốn tắt tính năng theo dõi URL không?', + disabledConfirm: 'Tính năng theo dõi URL đã được tắt.', + disableError: 'Lỗi khi tắt tính năng theo dõi URL, thông tin thêm có thể được tìm thấy trong tệp nhật ký của bạn.', + enabledConfirm: 'Tính năng theo dõi URL đã được bật.', + enableError: 'Lỗi khi bật tính năng theo dõi URL, thông tin thêm có thể được tìm thấy trong tệp nhật ký của bạn.', + culture: 'Ngôn ngữ', + }, + emptyStates: { + emptyDictionaryTree: 'Không có mục từ điển nào để chọn', + }, + textbox: { + characters_left: '%0% ký tự còn lại.', + characters_exceed: 'Tối đa %0% ký tự, %1% quá nhiều.', + }, + recycleBin: { + contentTrashed: 'Đã xóa nội dung với Id: {0} liên quan đến nội dung gốc với Id: {1}', + mediaTrashed: 'Đã xóa phương tiện với Id: {0} liên quan đến mục phương tiện gốc với Id: {1}', + itemCannotBeRestored: 'Không thể tự động khôi phục mục này', + itemCannotBeRestoredHelpText: + 'Không có vị trí nào mà mục này có thể được khôi phục tự động. Bạn có thể di chuyển mục này một cách thủ công bằng cách sử dụng cây bên dưới.', + wasRestored: 'đã được khôi phục dưới', + }, + relationType: { + direction: 'Hướng', + parentToChild: 'Cha đến con', + bidirectional: 'Hai chiều', + parent: 'Cha', + child: 'Con', + count: 'Số lượng', + relation: 'Mối quan hệ', + relations: 'Các mối quan hệ', + created: 'Đã tạo', + comment: 'Bình luận', + name: 'Tên', + noRelations: 'Không có mối quan hệ nào cho loại quan hệ này', + tabRelationType: 'Loại quan hệ', + tabRelations: 'Các mối quan hệ', + isDependency: 'Là phụ thuộc', + dependency: 'Có', + noDependency: 'Không', + }, + dashboardTabs: { + contentIntro: 'Bắt đầu', + contentRedirectManager: 'Quản lý URL chuyển hướng', + mediaFolderBrowser: 'Nội dung', + settingsWelcome: 'Chào mừng', + settingsExamine: 'Quản lý Examine', + settingsPublishedStatus: 'Trạng thái đã xuất bản', + settingsModelsBuilder: 'Models Builder', + settingsHealthCheck: 'Health Check', + settingsProfiler: 'Profiling', + memberIntro: 'Bắt đầu', + settingsAnalytics: 'Dữ liệu Telemetry', + }, + visuallyHiddenTexts: { + goBack: 'Quay lại', + activeListLayout: 'Bố cục đang dùng', + jumpTo: 'Nhảy đến', + group: 'Nhóm', + passed: 'Đã vượt qua', + warning: 'Cảnh báo', + failed: 'Thất bại', + suggestion: 'Gợi ý', + checkPassed: 'Kiểm tra đã vượt qua', + checkFailed: 'Kiểm tra thất bại', + openBackofficeSearch: 'Mở tìm kiếm backoffice', + openCloseBackofficeHelp: 'Mở/Đóng trợ giúp backoffice', + openCloseBackofficeProfileOptions: 'Mở/Đóng tùy chọn hồ sơ của bạn', + assignDomainDescription: 'Thiết lập Ngôn ngữ và Tên miền cho %0%', + createDescription: 'Tạo nút mới dưới %0%', + protectDescription: 'Thiết lập hạn chế truy cập trên %0%', + rightsDescription: 'Thiết lập quyền trên %0%', + sortDescription: 'Thay đổi thứ tự sắp xếp cho %0%', + createblueprintDescription: 'Tạo tài liệu mẫu dựa trên %0%', + openContextMenu: 'Mở menu ngữ cảnh cho %0%', + currentLanguage: 'Ngôn ngữ hiện tại', + switchLanguage: 'Chuyển ngôn ngữ sang', + createNewFolder: 'Tạo thư mục mới', + newPartialView: 'Partial View', + newPartialViewMacro: 'Partial View Macro', + newMember: 'Thành viên', + newDataType: 'Loại Dữ liệu', + redirectDashboardSearchLabel: 'Tìm kiếm bảng điều khiển chuyển hướng', + userGroupSearchLabel: 'Tìm kiếm phần nhóm người dùng', + userSearchLabel: 'Tìm kiếm phần người dùng', + createItem: 'Tạo mục', + create: 'Tạo mới', + edit: 'Chỉnh sửa', + name: 'Tên', + addNewRow: 'Thêm hàng mới', + tabExpand: 'Xem thêm tùy chọn', + searchOverlayTitle: 'Tìm kiếm trong Umbraco backoffice', + searchOverlayDescription: 'Tìm kiếm các nút nội dung, nút phương tiện, v.v. trong toàn bộ backoffice.', + searchInputDescription: + 'Khi có kết quả tự động hoàn thành, hãy nhấn phím mũi tên lên và xuống, hoặc sử dụng phím tab và phím enter để chọn.', + path: 'Đường dẫn:', + foundIn: 'Tìm thấy trong', + hasTranslation: 'Có bản dịch', + noTranslation: 'Thiếu bản dịch', + dictionaryListCaption: 'Các mục từ điển', + contextMenuDescription: 'Chọn một trong các tùy chọn để chỉnh sửa nút.', + contextDialogDescription: 'Thực hiện hành động %0% trên nút %1%', + addImageCaption: 'Thêm chú thích hình ảnh', + searchContentTree: 'Tìm kiếm cây nội dung', + maxAmount: 'Số lượng tối đa', + expandChildItems: 'Mở rộng các mục con cho', + openContextNode: 'Mở nút ngữ cảnh cho %0%', + }, + references: { + tabName: 'Tham chiếu', + DataTypeNoReferences: 'Kiểu dữ liệu này không có tham chiếu nào.', + itemHasNoReferences: 'Mục này không có tham chiếu nào.', + labelUsedByDocumentTypes: 'Được sử dụng trong các Loại Tài liệu', + labelUsedByMediaTypes: 'Được sử dụng trong các Loại Phương tiện', + labelUsedByMemberTypes: 'Được sử dụng trong các Loại Thành viên', + usedByProperties: 'Được sử dụng bởi', + labelUsedByDocuments: 'Được sử dụng trong các Tài liệu', + labelUsedByMembers: 'Được sử dụng trong các Thành viên', + labelUsedByMedia: 'Được sử dụng trong Phương tiện', + labelUsedItems: 'Các mục đang được sử dụng', + labelUsedDescendants: 'Các node con đang được sử dụng', + deleteWarning: + 'Mục này hoặc các phần tử con của nó đang được sử dụng. Việc xóa có thể gây ra liên kết hỏng trên website của bạn.', + unpublishWarning: + 'Mục này hoặc các phần tử con của nó đang được sử dụng. Việc hủy xuất bản có thể gây ra liên kết hỏng trên website của bạn. Vui lòng thực hiện các hành động phù hợp.', + deleteDisabledWarning: 'Mục này hoặc các phần tử con của nó đang được sử dụng. Do đó, việc xóa đã bị tắt.', + listViewDialogWarning: 'Các mục sau mà bạn đang cố gắng %0% đang được sử dụng bởi nội dung khác.', + labelUsedByItems: 'Được liên kết từ các mục sau', + labelDependsOnThis: 'Các mục sau phụ thuộc vào mục này', + labelDependentDescendants: 'Các mục con sau có phụ thuộc', + labelMoreReferences: (count: number) => { + if (count === 1) return '...và một mục nữa'; + return `...và ${count} mục nữa`; + }, + }, + logViewer: { + deleteSavedSearch: 'Xóa Tìm kiếm Đã lưu', + logLevels: 'Cấp độ Log', + selectAllLogLevelFilters: 'Chọn tất cả', + deselectAllLogLevelFilters: 'Bỏ chọn tất cả', + savedSearches: 'Tìm kiếm Đã lưu', + saveSearch: 'Lưu Tìm kiếm', + saveSearchDescription: 'Nhập tên dễ nhận biết cho truy vấn tìm kiếm của bạn.', + filterSearch: 'Lọc tìm kiếm', + totalItems: 'Tổng số mục', + timestamp: 'Thời gian', + level: 'Cấp độ', + machine: 'Máy', + message: 'Tin nhắn', + exception: 'Ngoại lệ', + properties: 'Thuộc tính', + searchWithGoogle: 'Tìm kiếm với Google', + searchThisMessageWithGoogle: 'Tìm kiếm tin nhắn này với Google', + searchWithBing: 'Tìm kiếm với Bing', + searchThisMessageWithBing: 'Tìm kiếm tin nhắn này bằng Bing', + searchOurUmbraco: 'Tìm kiếm Our Umbraco', + searchThisMessageOnOurUmbracoForumsAndDocs: 'Tìm kiếm thông báo này trên diễn đàn và tài liệu Our Umbraco', + searchOurUmbracoWithGoogle: 'Tìm kiếm Our Umbraco bằng Google', + searchOurUmbracoForumsUsingGoogle: 'Tìm kiếm diễn đàn Our Umbraco bằng Google', + searchUmbracoSource: 'Tìm kiếm mã nguồn Umbraco', + searchWithinUmbracoSourceCodeOnGithub: 'Tìm trong mã nguồn Umbraco trên GitHub', + searchUmbracoIssues: 'Tìm kiếm vấn đề của Umbraco', + searchUmbracoIssuesOnGithub: 'Tìm vấn đề của Umbraco trên GitHub', + deleteThisSearch: 'Xóa tìm kiếm này', + findLogsWithRequestId: 'Tìm log với Request ID', + findLogsWithNamespace: 'Tìm log với Namespace', + findLogsWithMachineName: 'Tìm log với tên máy', + open: 'Mở', + polling: 'Thăm dò', + every2: 'Mỗi 2 giây', + every5: 'Mỗi 5 giây', + every10: 'Mỗi 10 giây', + every20: 'Mỗi 20 giây', + every30: 'Mỗi 30 giây', + pollingEvery2: 'Thăm dò mỗi 2 giây', + pollingEvery5: 'Thăm dò mỗi 5 giây', + pollingEvery10: 'Thăm dò mỗi 10 giây', + pollingEvery20: 'Thăm dò mỗi 20 giây', + pollingEvery30: 'Thăm dò mỗi 30 giây', + }, + clipboard: { + labelForCopyAllEntries: 'Sao chép %0%', + labelForArrayOfItemsFrom: '%0% từ %1%', + labelForArrayOfItems: 'Tập hợp %0%', + labelForRemoveAllEntries: 'Xóa tất cả mục', + labelForClearClipboard: 'Xóa clipboard', + labelForCopyToClipboard: 'Sao chép vào clipboard', + confirmDeleteHeadline: 'Xóa khỏi clipboard', + confirmDeleteDescription: 'Bạn có chắc chắn muốn xóa {0} khỏi clipboard không?', + copySuccessHeadline: 'Đã sao chép vào clipboard', + }, + propertyActions: { + tooltipForPropertyActionsMenu: 'Mở thao tác thuộc tính', + tooltipForPropertyActionsMenuClose: 'Đóng thao tác thuộc tính', + }, + nuCache: { + refreshStatus: 'Làm mới trạng thái', + memoryCache: 'Bộ nhớ đệm', + memoryCacheDescription: + '"Nút này cho phép bạn tải lại bộ nhớ đệm trong RAM, bằng cách tải lại hoàn toàn từ bộ nhớ đệm cơ sở dữ liệu (nhưng không xây dựng lại bộ nhớ đệm cơ sở dữ liệu đó). Quá trình này tương đối nhanh. Sử dụng khi bạn nghĩ rằng bộ nhớ đệm trong RAM chưa được làm mới đúng cách sau một số sự kiện đã xảy ra — điều này có thể chỉ ra một vấn đề nhỏ trong Umbraco. (Lưu ý: thao tác này sẽ kích hoạt tải lại trên tất cả các server trong môi trường cân bằng tải, và sẽ xóa bộ nhớ đệm cấp hai nếu bạn đã bật nó)', + reload: 'Tải lại', + databaseCache: 'Bộ nhớ đệm cơ sở dữ liệu', + databaseCacheDescription: + 'Nút này cho phép bạn xây dựng lại bộ nhớ đệm cơ sở dữ liệu, ví dụ như nội dung của bảng cmsContentNu. Quá trình xây dựng lại có thể tốn kém. Sử dụng khi việc tải lại không đủ và bạn nghĩ rằng bộ nhớ đệm cơ sở dữ liệu chưa được tạo đúng cách — điều này có thể chỉ ra một vấn đề nghiêm trọng trong Umbraco.', + rebuild: 'Xây dựng lại', + internals: 'Nội bộ', + internalsDescription: + 'Nút này cho phép bạn kích hoạt việc thu thập snapshot của NuCache (sau khi chạy fullCLR GC). Trừ khi bạn hiểu rõ điều này có nghĩa là gì, còn không thì có lẽ bạn không cần dùng đến.', + collect: 'Thu thập', + publishedCacheStatus: 'Trạng thái bộ nhớ đệm đã xuất bản', + caches: 'Bộ nhớ đệm', + }, + profiling: { + performanceProfiling: 'Phân tích hiệu suất', + performanceProfilingDescription: + '

Hiện tại Umbraco đang chạy ở chế độ gỡ lỗi (debug mode). Điều này có nghĩa là bạn có thể sử dụng trình phân tích hiệu suất tích hợp để đánh giá hiệu suất khi hiển thị các trang.

Nếu bạn muốn kích hoạt trình phân tích cho một lần hiển thị trang cụ thể, chỉ cần thêm umbDebug=true vào chuỗi truy vấn khi yêu cầu trang.

Nếu bạn muốn trình phân tích được kích hoạt theo mặc định cho tất cả các lần hiển thị trang, bạn có thể sử dụng công tắc bên dưới. Nó sẽ đặt một cookie trong trình duyệt của bạn, cookie này sẽ tự động kích hoạt trình phân tích. Nói cách khác, trình phân tích sẽ chỉ hoạt động theo mặc định trong trình duyệt của bạn - không phải của người khác.

', + activateByDefault: 'Kích hoạt trình phân tích theo mặc định', + reminder: 'Nhắc nhở thân thiện', + reminderDescription: + '

Bạn không bao giờ nên để một trang web sản xuất chạy ở chế độ gỡ lỗi. Chế độ gỡ lỗi được tắt bằng cách đặt Umbraco:CMS:Hosting:Debug thành false trong appsettings.json, appsettings.{Environment}.json hoặc thông qua một biến môi trường.

', + profilerEnabledDescription: + '

Umbraco hiện tại không chạy ở chế độ gỡ lỗi, vì vậy bạn không thể sử dụng trình phân tích tích hợp. Đây là cách mà nó nên hoạt động cho một trang web sản xuất.

Chế độ gỡ lỗi được bật bằng cách đặt Umbraco:CMS:Hosting:Debug thành true trong appsettings.json, appsettings.{Environment}.json hoặc thông qua một biến môi trường.

', + }, + settingsDashboardVideos: { + trainingHeadline: 'Hàng giờ video hướng dẫn Umbraco chỉ cách bạn một cú nhấp chuột', + trainingDescription: + '

Bạn muốn làm chủ Umbraco? Hãy dành vài phút để tìm hiểu một số phương pháp hay nhất bằng cách xem một trong những video này về việc sử dụng Umbraco. Và hãy truy cập umbraco.tv để xem thêm nhiều video về Umbraco hơn

', + learningBaseDescription: + '

Bạn muốn làm chủ Umbraco? Hãy dành vài phút để tìm hiểu một số phương pháp hay nhất bằng cách truy cập kênh Youtube Umbraco Learning Base. Tại đây, bạn có thể tìm thấy rất nhiều tài liệu video bao quát nhiều khía cạnh của Umbraco.

', + getStarted: 'Để giúp bạn bắt đầu', + }, + settingsDashboard: { + documentationHeader: 'Tài liệu', + documentationDescription: 'Đọc thêm về cách làm việc với các mục trong Cài đặt trong Tài liệu của chúng tôi.', + communityHeader: 'Cộng đồng', + communityDescription: 'Đặt câu hỏi trong diễn đàn cộng đồng hoặc cộng đồng Discord của chúng tôi.', + trainingHeader: 'Đào tạo', + trainingDescription: 'Tìm hiểu về các khóa đào tạo và chứng nhận trong thực tế', + supportHeader: 'Hỗ trợ', + supportDescription: 'Mở rộng đội ngũ của bạn với một nhóm những người am hiểu và đam mê về Umbraco.', + videosHeader: 'Videos', + videosDescription: + 'Xem các video hướng dẫn miễn phí của chúng tôi trên kênh YouTube Umbraco Learning Base, để nhanh chóng làm quen với Umbraco.', + getHelp: 'Nhận trợ giúp bạn cần', + getCertified: 'Nhận Chứng nhận', + goForum: 'Đi đến diễn đàn', + chatWithCommunity: 'Trò chuyện với cộng đồng', + watchVideos: 'Xem các video', + }, + startupDashboard: { + fallbackHeadline: 'Chào mừng bạn đến với The Friendly CMS', + fallbackDescription: + 'Cảm ơn bạn đã chọn Umbraco - chúng tôi nghĩ rằng đây có thể là khởi đầu của một điều gì đó tuyệt đẹp. Mặc dù có thể cảm thấy choáng ngợp vào đầu, nhưng chúng tôi đã làm rất nhiều để làm cho đường cong học tập trở nên mượt mà và nhanh chóng nhất có thể.', + }, + welcomeDashboard: { + ourUmbracoHeadline: 'Our Umbraco – Cộng đồng thân thiện nhất', + ourUmbracoDescription: + 'Our Umbraco, trang cộng đồng chính thức, là nơi tổng hợp mọi thứ về Umbraco. Dù bạn cần giải đáp thắc mắc, plugin thú vị hay hướng dẫn cách thực hiện trong Umbraco, cộng đồng thân thiện và tuyệt vời nhất thế giới chỉ cách bạn một cú nhấp chuột.', + ourUmbracoButton: 'Truy cập Our Umbraco', + documentationHeadline: 'Tài liệu', + documentationDescription: 'Tìm câu trả lời cho mọi thắc mắc về Umbraco của bạn', + communityHeadline: 'Cộng đồng', + communityDescription: 'Nhận hỗ trợ và cảm hứng từ các chuyên gia Umbraco', + resourcesHeadline: 'Tài nguyên', + resourcesDescription: 'Video hướng dẫn miễn phí để khởi đầu hành trình của bạn với CMS', + trainingHeadline: 'Đào tạo', + trainingDescription: 'Khóa đào tạo thực tế và chứng chỉ chính thức của Umbraco', + }, + blockEditor: { + headlineCreateBlock: 'Chọn Element Type', + headlineAddSettingsElementType: 'Gắn Element Type cho cài đặt', + headlineAddCustomView: 'Chọn chế độ xem', + headlineAddCustomStylesheet: 'Chọn stylesheet', + headlineAddThumbnail: 'Chọn ảnh thu nhỏ', + labelcreateNewElementType: 'Tạo Element Type mới', + labelCustomStylesheet: 'Stylesheet tùy chỉnh', + addCustomStylesheet: 'Thêm stylesheet', + headlineEditorAppearance: 'Giao diện khối', + headlineDataModels: 'Mô hình dữ liệu', + headlineCatalogueAppearance: 'Giao diện danh mục', + labelBackgroundColor: 'Màu nền', + labelIconColor: 'Màu biểu tượng', + labelContentElementType: 'Mô hình nội dung', + labelLabelTemplate: 'Nhãn', + labelCustomView: 'Chế độ xem tùy chỉnh', + labelCustomViewInfoTitle: 'Hiển thị mô tả chế độ xem tùy chỉnh', + labelCustomViewDescription: + 'Ghi đè cách khối này hiển thị trong giao diện quản trị. Chọn một tệp .html chứa phần trình bày của bạn.', + labelSettingsElementType: 'Mô hình cài đặt', + labelEditorSize: 'Kích thước trình chỉnh sửa overlay', + addCustomView: 'Thêm chế độ xem tùy chỉnh', + addSettingsElementType: 'Thêm cài đặt', + confirmDeleteBlockTitle: 'Xóa %0%?', + confirmDeleteBlockMessage: 'Bạn có chắc chắn muốn xóa %0% này không?', + confirmDeleteBlockTypeMessage: 'Bạn có chắc chắn muốn xóa cấu hình khối %0% không?', + confirmDeleteBlockTypeNotice: + 'Nội dung của khối này vẫn sẽ được giữ lại, nhưng bạn sẽ không thể chỉnh sửa nữa và nó sẽ được hiển thị là nội dung không được hỗ trợ.', + confirmDeleteBlockGroupTitle: 'Xóa nhóm?', + confirmDeleteBlockGroupMessage: + 'Bạn có chắc chắn muốn xóa nhóm %0% và tất cả cấu hình khối bên trong nhóm này không?', + confirmDeleteBlockGroupNotice: + 'Nội dung của các khối này vẫn sẽ được giữ lại, nhưng bạn sẽ không thể chỉnh sửa nữa và nó sẽ được hiển thị là nội dung không được hỗ trợ.', + blockConfigurationOverlayTitle: "Cấu hình của '%0%'", + elementTypeDoesNotExist: 'Không thể chỉnh sửa vì ElementType không tồn tại.', + thumbnail: 'Ảnh thu nhỏ', + addThumbnail: 'Thêm ảnh thu nhỏ', + tabCreateEmpty: 'Tạo rỗng', + tabClipboard: 'Clipboard', + tabBlockSettings: 'Cài đặt', + headlineAdvanced: 'Nâng cao', + headlineCustomView: 'Chế độ xem tùy chỉnh', + forceHideContentEditor: 'Ẩn trình chỉnh sửa nội dung', + forceHideContentEditorHelp: + 'Ẩn nút chỉnh sửa nội dung và trình chỉnh sửa nội dung khỏi cửa sổ Block Editor overlay.', + gridInlineEditing: 'Chỉnh sửa trực tiếp', + gridInlineEditingHelp: + 'Cho phép chỉnh sửa trực tiếp thuộc tính đầu tiên. Các thuộc tính bổ sung có thể được chỉnh sửa trong cửa sổ overlay.', + blockHasChanges: 'Bạn đã thực hiện thay đổi cho nội dung này. Bạn có chắc chắn muốn hủy các thay đổi đó không?', + confirmCancelBlockCreationHeadline: 'Hủy tạo?', + confirmCancelBlockCreationMessage: 'Bạn có chắc chắn muốn hủy việc tạo mới không?', + elementTypeDoesNotExistHeadline: 'Lỗi!', + elementTypeDoesNotExistDescription: 'Element Type của khối này không còn tồn tại nữa', + addBlock: 'Thêm nội dung', + addThis: 'Thêm %0%', + propertyEditorNotSupported: + "Thuộc tính '%0%' đang sử dụng trình chỉnh sửa '%1%', nhưng trình chỉnh sửa này không được hỗ trợ trong Blocks.", + focusParentBlock: 'Đặt tiêu điểm vào khối chứa', + areaIdentification: 'Định danh', + areaValidation: 'Xác thực', + areaValidationEntriesShort: '%0% phải xuất hiện ít nhất %2% lần.', + areaValidationEntriesExceed: '%0% phải tối đa xuất hiện %3% lần.', + areaNumberOfBlocks: 'Số lượng khối', + areaDisallowAllBlocks: 'Chỉ cho phép các loại khối cụ thể', + areaAllowedBlocks: 'Các loại khối được phép', + areaAllowedBlocksHelp: + 'Xác định các loại khối được phép trong khu vực này, và tùy chọn số lượng của từng loại khối cần có.', + confirmDeleteBlockAreaMessage: 'Bạn có chắc chắn muốn xóa khu vực này không?', + confirmDeleteBlockAreaNotice: 'Mọi khối hiện có trong khu vực này sẽ bị xóa.', + layoutOptions: 'Tùy chọn bố cục', + structuralOptions: 'Cấu trúc', + sizeOptions: 'Tùy chọn kích thước', + sizeOptionsHelp: 'Xác định một hoặc nhiều tùy chọn kích thước, điều này cho phép thay đổi kích thước của Khối', + allowedBlockColumns: 'Các khoảng cột khả dụng', + allowedBlockColumnsHelp: + 'Xác định số cột khác nhau mà khối này được phép chiếm. Cấu hình này không ngăn việc đặt khối vào các Khu vực có số cột nhỏ hơn.', + allowedBlockRows: 'Số hàng có thể chiếm', + allowedBlockRowsHelp: 'Xác định phạm vi hàng bố cục mà khối này được phép kéo dài qua.', + allowBlockInRoot: 'Cho phép trong root', + allowBlockInRootHelp: 'Làm cho khối này khả dụng trong root của bố cục.', + allowBlockInAreas: 'Cho phép trong khu vực', + allowBlockInAreasHelp: + 'Làm cho khối này khả dụng theo mặc định trong các Khu vực của các Khối khác (trừ khi có thiết lập quyền rõ ràng cho các Khu vực đó).', + areaAllowedBlocksEmpty: + 'Theo mặc định, tất cả các loại khối đều được phép trong một Khu vực. Sử dụng tùy chọn này để chỉ cho phép các loại được chọn.', + areas: 'Khu vực', + areasLayoutColumns: 'Số cột của Khu vực', + areasLayoutColumnsHelp: + 'Xác định số lượng cột sẽ khả dụng cho các Khu vực. Nếu không được xác định, số lượng cột được định nghĩa cho toàn bộ bố cục sẽ được sử dụng.', + areasConfigurations: 'Khu vực', + areasConfigurationsHelp: + "Để cho phép lồng các khối (blocks) bên trong khối này, hãy định nghĩa một hoặc nhiều khu vực (areas). Các khu vực sẽ tuân theo bố cục được xác định bởi cấu hình cột của chính chúng. 'Độ giãn cột' (column span) và 'độ giãn hàng' (row span) cho từng khu vực có thể được điều chỉnh bằng hộp kéo-thả (scale-handler) ở góc dưới bên phải của khu vực đã chọn.", + invalidDropPosition: '%0% không được phép tại vị trí này.', + defaultLayoutStylesheet: 'Stylesheet bố cục mặc định', + confirmPasteDisallowedNestedBlockHeadline: 'Nội dung không được phép đã bị từ chối', + confirmPasteDisallowedNestedBlockMessage: + 'Nội dung được chèn có chứa phần không được phép, nên chưa được tạo. Bạn có muốn giữ lại phần nội dung còn lại không?', + areaAliasHelp: + 'Khi sử dụng GetBlockGridHTML() để render Block Grid, alias sẽ được render trong markup dưới dạng thuộc tính \'data-area-alias\'. Sử dụng thuộc tính alias để nhắm đến phần tử cho khu vực. Ví dụ. .umb-block-grid__area[data-area-alias="MyAreaAlias"] { ... }', + scaleHandlerButtonTitle: 'Kéo để thay đổi kích thước', + areaCreateLabelTitle: 'Tạo Button Label', + areaCreateLabelHelp: "Ghi đè văn bản nhãn khi thêm một Block mới vào Khu vực này. Ví dụ: 'Thêm Widget'", + showSizeOptions: 'Hiển thị tùy chọn thay đổi kích thước', + addBlockType: 'Thêm Khối (Block)', + addBlockGroup: 'Thêm nhóm', + pickSpecificAllowance: 'Chọn nhóm hoặc Khối (Block)', + allowanceMinimum: 'Thiết lập giới hạn tối thiểu', + allowanceMaximum: 'Thiết lập giới hạn tối đa', + block: 'Khối (Block)', + tabBlock: 'Khối (Block)', + tabBlockTypeSettings: 'Cài đặt', + tabAreas: 'Khu vực', + tabAdvanced: 'Nâng cao', + headlineAllowance: 'Quyền hạn', + getSampleHeadline: 'Cài đặt cấu hình mẫu', + getSampleDescription: + 'Thao tác này sẽ thêm các Khối cơ bản để bạn bắt đầu làm việc với Block Grid Editor. Các Khối bao gồm: Tiêu đề, Văn bản định dạng, Hình ảnh và Bố cục hai cột.', + getSampleButton: 'Cài đặt', + actionEnterSortMode: 'Chế độ sắp xếp', + actionExitSortMode: 'Thoát chế độ sắp xếp', + areaAliasIsNotUnique: 'Alias của Khu vực này phải là duy nhất so với các Khu vực khác trong Block này.', + configureArea: 'Cấu hình khu vực', + deleteArea: 'Xóa khu vực', + addColumnSpanOption: 'Thêm tùy chọn trải rộng %0% cột', + createThisFor: (name: string, variantName: string) => + variantName ? `Khởi tạo ${name} cho ${variantName}` : `Tạo ${name}`, + insertBlock: 'Chèn Khối', + labelInlineMode: 'Hiển thị cùng dòng với văn bản', + notExposedLabel: 'Bản nháp', + notExposedDescription: 'Khối này chưa được tạo cho biến thể này', + areaValidationEntriesNotAllowed: '%0% không được phép trong khu vực này.', + rootValidationEntriesNotAllowed: '%0% không được phép trong gốc của thuộc tính này.', + unsupportedBlockName: 'Không được hỗ trợ', + unsupportedBlockDescription: + 'Nội dung này không còn được hỗ trợ trong Trình soạn thảo. Nếu bạn bị thiếu nội dung này, vui lòng liên hệ với quản trị viên. Nếu không, hãy xóa nó.', + blockVariantConfigurationNotSupported: + 'Một hoặc nhiều Loại khối trong Trình soạn khối đang sử dụng Loại phần tử được cấu hình thay đổi theo Ngôn ngữ hoặc theo Phân đoạn. Điều này không được hỗ trợ đối với nội dung không thay đổi theo Ngôn ngữ hoặc Phân đoạn.', + }, + contentTemplatesDashboard: { + whatHeadline: 'Mẫu tài liệu là gì?', + whatDescription: 'Mẫu tài liệu là nội dung định sẵn, bạn có thể chọn khi tạo nội dung mới.', + createHeadline: 'Làm thế nào để tôi tạo một Mẫu tài liệu?', + createDescription: + '

Có hai cách để tạo một Mẫu tài liệu:

  • Nhấp chuột phải vào một nút nội dung và chọn "Tạo Mẫu tài liệu" để tạo một Mẫu tài liệu mới.
  • Nhấp chuột phải vào cây Mẫu tài liệu trong phần Cài đặt và chọn Loại tài liệu mà bạn muốn tạo Mẫu tài liệu cho.

Sau khi được đặt tên, các biên tập viên có thể bắt đầu sử dụng Mẫu tài liệu như một nền tảng cho trang mới của họ.

', + manageHeadline: 'Làm thế nào để tôi quản lý các Mẫu tài liệu?', + manageDescription: + 'Bạn có thể chỉnh sửa và xóa Mẫu tài liệu từ cây "Mẫu tài liệu" trong phần Cài đặt. Mở rộng Loại tài liệu mà Mẫu tài liệu dựa trên đó và nhấp vào để chỉnh sửa hoặc xóa.', + }, + preview: { + endLabel: 'Kết thúc', + endTitle: 'Kết thúc chế độ xem trước', + openWebsiteLabel: 'Xem trước trang web', + openWebsiteTitle: 'Mở trang web ở chế độ xem trước', + returnToPreviewHeadline: 'Xem trước trang web?', + returnToPreviewDescription: + 'Bạn đã kết thúc chế độ xem trước, bạn có muốn bật lại để xem phiên bản mới nhất đã lưu của trang web của mình không?', + returnToPreviewAcceptButton: 'Xem trước phiên bản mới nhất', + returnToPreviewDeclineButton: 'Xem phiên bản đã xuất bản', + viewPublishedContentHeadline: 'Xem phiên bản đã được xuất bản?', + viewPublishedContentDescription: + 'Bạn đang ở chế độ xem trước, bạn có muốn thoát để xem phiên bản đã xuất bản của trang web của mình không?', + viewPublishedContentAcceptButton: 'Xem phiên bản đã xuất bản', + viewPublishedContentDeclineButton: 'Giữ nguyên ở chế độ xem trước', + }, + permissions: { + FolderCreation: 'Tạo thư mục', + FileWritingForPackages: 'Ghi tệp cho gói', + FileWriting: 'Ghi tệp', + MediaFolderCreation: 'Tạo thư mục phương tiện', + }, + treeSearch: { + searchResult: 'mục được trả về', + searchResults: 'các mục được trả về', + }, + analytics: { + consentForAnalytics: 'Cho phép thu thập dữ liệu telemetry', + analyticsLevelSavedSuccess: 'Mức độ telemetry đã được lưu!', + analyticsDescription: + 'Để cải thiện Umbraco và bổ sung các chức năng mới dựa trên thông tin phù hợp nhất, chúng tôi muốn thu thập thông tin về hệ thống và cách sử dụng từ cài đặt của bạn..
Dữ liệu tổng hợp sẽ được chia sẻ định kỳ, cùng với những bài học rút ra từ các chỉ số này.
Hy vọng bạn sẽ giúp chúng tôi thu thập những dữ liệu giá trị.', + minimalLevelDescription: 'Chúng tôi chỉ gửi một ID trang ẩn danh để cho biết rằng trang web tồn tại.', + basicLevelDescription: 'Chúng tôi sẽ gửi một ID trang ẩn danh, phiên bản Umbraco và các gói đã cài đặt', + detailedLevelDescription: + 'Chúng tôi sẽ gửi:
  • ID trang web đã được ẩn danh, phiên bản Umbraco và các gói đã cài đặt.
  • Các số liệu: Số lượng nút gốc, nút nội dung, media, loại tài liệu, mẫu (template), ngôn ngữ, miền, nhóm người dùng, người dùng, thành viên, nhà cung cấp đăng nhập ngoài cho backoffice, và các trình chỉnh sửa thuộc tính đang được sử dụng.
  • Thông tin hệ thống: Máy chủ web, hệ điều hành máy chủ, framework của máy chủ, ngôn ngữ hệ điều hành máy chủ, và nhà cung cấp cơ sở dữ liệu.
  • Các thiết lập cấu hình: Chế độ ModelsBuilder, có hay không đường dẫn Umbraco tùy chỉnh, môi trường ASP, trạng thái bật/tắt của Delivery API và quyền truy cập công khai, cũng như trạng thái chạy chế độ debug.
Trong tương lai, chúng tôi có thể thay đổi nội dung dữ liệu được gửi ở mức “Chi tiết”. Nếu có, những thay đổi đó sẽ được liệt kê ở trên.
Bằng cách chọn “Chi tiết”, bạn đồng ý cho phép thu thập thông tin ẩn danh hiện tại và trong tương lai.
', + }, + routing: { + routeNotFoundTitle: 'Không tìm thấy nội dung', + routeNotFoundDescription: 'Không tìm thấy đường dẫn yêu cầu. Vui lòng kiểm tra lại URL và thử lại.', + routeForbiddenTitle: 'Truy cập bị từ chối', + routeForbiddenDescription: + 'Bạn không có quyền truy cập tài nguyên này. Vui lòng liên hệ với quản trị viên để được hỗ trợ.', + }, + codeEditor: { + label: 'Trình chỉnh sửa mã', + languageConfigLabel: 'Ngôn ngữ', + languageConfigDescription: 'Chọn ngôn ngữ cho đánh dấu cú pháp và IntelliSense.', + heightConfigLabel: 'Chiều cao', + heightConfigDescription: 'Đặt chiều cao của trình chỉnh sửa mã theo pixel.', + lineNumbersConfigLabel: 'Số dòng', + lineNumbersConfigDescription: 'Hiển thị số dòng trong trình chỉnh sửa mã.', + minimapConfigLabel: 'Minimap', + minimapConfigDescription: 'Hiển thị Minimap trong trình soạn thảo mã.', + wordWrapConfigLabel: 'Ngắt dòng tự động', + wordWrapConfigDescription: 'Bật chế độ Ngắt dòng tự động trong trình chỉnh sửa mã.', + }, + rte: { + config_blocks: 'Khối khả dụng', + config_blocks_description: 'Định nghĩa các khối khả dụng', + config_ignoreUserStartNodes: 'Bỏ qua nút bắt đầu của người dùng', + config_maxImageSize: 'Kích thước tối đa cho hình ảnh được chèn', + config_maxImageSize_description: 'Chiều rộng hoặc chiều cao tối đa - nhập 0 để vô hiệu hóa thay đổi kích thước.', + config_mediaParentId: 'Thư mục tải lên hình ảnh', + config_mediaParentId_description: 'Chọn vị trí tải lên của hình ảnh đã dán.', + config_overlaySize: 'Kích thước lớp phủ', + config_overlaySize_description: 'Chọn chiều rộng của lớp phủ (trình chọn liên kết).', + }, + tiptap: { + anchor: 'Anchor', + anchor_input: 'Nhập anchor ID', + config_dimensions_description: + 'Thiết lập chiều rộng và chiều cao cố định của trình soạn thảo. Không bao gồm chiều cao của thanh công cụ và thanh trạng thái.', + config_extensions: 'Khả năng', + config_statusbar: 'Statusbar', + config_toolbar: 'Toolbar', + extGroup_formatting: 'Định dạng văn bản', + extGroup_interactive: 'Các phần tử tương tác', + extGroup_media: 'Nhúng và phương tiện', + extGroup_structure: 'Cấu trúc nội dung', + extGroup_unknown: 'Chưa phân loại', + statusbar_availableItems: 'Trạng thái có sẵn', + statusbar_availableItemsEmpty: 'Không có phần mở rộng statusbar nào để hiển thị', + toolbar_availableItems: 'Thao tác khả dụng', + toolbar_availableItemsEmpty: 'Không có phần mở rộng toolbar nào để hiển thị', + toolbar_designer: 'Thiết kế thanh công cụ', + toolbar_addRow: 'Thêm hàng', + toolbar_addGroup: 'Thêm nhóm', + toolbar_addItems: 'Thêm hành động', + toolbar_removeRow: 'Xóa dòng', + toolbar_removeGroup: 'Xóa nhóm', + toolbar_removeItem: 'Xóa hành động', + toolbar_emptyGroup: 'Trống', + sourceCodeEdit: 'Chỉnh sửa mã nguồn', + charmap: 'Bảng ký tự', + charmap_headline: 'Ký tự đặc biệt', + charmap_currency: 'Tiền tệ', + charmap_text: 'Văn bản', + charmap_quotations: 'Đoạn trích', + charmap_maths: 'Toán học', + charmap_extlatin: 'Latinh mở rộng', + charmap_symbols: 'Ký hiệu', + charmap_arrows: 'Mũi tên', + statusbar_characters: (count: number) => `${count.toLocaleString()} ${count === 1 ? 'ký tự' : 'ký tự'}`, + statusbar_words: (count: number) => `${count.toLocaleString()} ${count === 1 ? 'từ' : 'từ'}`, + }, + linkPicker: { + modalSource: 'Nguồn', + modalManual: 'Thủ công', + modalAnchorValidationMessage: + 'Vui lòng nhập anchor hoặc querystring, chọn một tài liệu hoặc mục media, hoặc tự cấu hình URL.', + resetUrlHeadline: 'Đặt lại URL?', + resetUrlMessage: 'Bạn có chắc chắn muốn đặt lại URL này không?', + resetUrlLabel: 'Đặt lại', + }, + uiCulture: { + ar: 'العربية', + bs: 'Bosanski', + cs: 'Česky', + cy: 'Cymraeg', + da: 'Dansk', + de: 'Deutsch', + en: 'English (UK)', + 'en-us': 'English (US)', + es: 'Español', + fr: 'Français', + he: 'Hebrew', + hr: 'Hrvatski', + it: 'Italiano', + ja: '日本語', + ko: '한국어', + nb: 'Norsk Bokmål', + nl: 'Nederlands', + pl: 'Polski', + pt: 'Português', + 'pt-br': 'Português (Brasil)', + ro: 'Romana', + ru: 'Русский', + sv: 'Svenska', + tr: 'Türkçe', + uk: 'Українська', + zh: '中文', + 'zh-tw': '中文(正體,台灣)', + vi: 'Tiếng Việt', + }, +} as UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/install.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/install.handlers.ts index 8a259c12f4..6866a2071c 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/install.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/install.handlers.ts @@ -45,6 +45,7 @@ export const handlers = [ serverPlaceholder: '', requiresCredentials: false, supportsIntegratedAuthentication: false, + supportsTrustServerCertificate: false, requiresConnectionTest: false, }, { @@ -58,6 +59,7 @@ export const handlers = [ serverPlaceholder: '(local)\\SQLEXPRESS', requiresCredentials: true, supportsIntegratedAuthentication: true, + supportsTrustServerCertificate: true, requiresConnectionTest: true, }, { @@ -71,6 +73,7 @@ export const handlers = [ serverPlaceholder: 'undefined', requiresCredentials: false, supportsIntegratedAuthentication: false, + supportsTrustServerCertificate: false, requiresConnectionTest: true, }, ], diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context.ts index aca05d1ea2..9528926842 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context.ts @@ -6,10 +6,8 @@ import { } from '../../constants.js'; import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from '../../block-grid-manager/block-grid-manager.context-token.js'; import { UMB_BLOCK_GRID_ENTRIES_CONTEXT } from '../block-grid-entries/block-grid-entries.context-token.js'; -import { - type UmbBlockGridScalableContext, - UmbBlockGridScaleManager, -} from '../../context/block-grid-scale-manager/block-grid-scale-manager.controller.js'; +import { UmbBlockGridScaleManager } from '../../context/block-grid-scale-manager/block-grid-scale-manager.controller.js'; +import type { UmbBlockGridScalableContext } from '../../context/block-grid-scale-manager/block-grid-scale-manager.controller.js'; import { UmbArrayState, UmbBooleanState, @@ -18,10 +16,10 @@ import { mergeObservables, observeMultiple, } from '@umbraco-cms/backoffice/observable-api'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbBlockEntryContext } from '@umbraco-cms/backoffice/block'; import { UMB_PROPERTY_CONTEXT, UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; import { UMB_CLIPBOARD_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/clipboard'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbBlockGridEntryContext extends UmbBlockEntryContext< @@ -301,7 +299,7 @@ export class UmbBlockGridEntryContext const workspaceName = propertyDatasetContext?.getName(); const propertyLabel = propertyContext?.getLabel(); - const blockLabel = this.getLabel(); + const blockLabel = this.getName(); const entryName = workspaceName ? `${workspaceName} - ${propertyLabel} - ${blockLabel}` diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts index 783c0ab10f..5e3a68e444 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts @@ -5,24 +5,25 @@ import { UMB_BLOCK_LIST_PROPERTY_EDITOR_SCHEMA_ALIAS, UMB_BLOCK_LIST_PROPERTY_EDITOR_UI_ALIAS, } from '../../constants.js'; +import { css, customElement, html, nothing, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement, umbDestroyOnDisconnect } from '@umbraco-cms/backoffice/lit-element'; -import { html, css, customElement, property, state, nothing } from '@umbraco-cms/backoffice/external/lit'; -import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils'; -import { UmbObserveValidationStateController } from '@umbraco-cms/backoffice/validation'; import { UmbDataPathBlockElementDataQuery } from '@umbraco-cms/backoffice/block'; +import { UmbObserveValidationStateController } from '@umbraco-cms/backoffice/validation'; +import { UUIBlinkAnimationValue, UUIBlinkKeyframes } from '@umbraco-cms/backoffice/external/uui'; +import { UMB_PROPERTY_CONTEXT, UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; +import { UMB_CLIPBOARD_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/clipboard'; import type { ManifestBlockEditorCustomView, UmbBlockEditorCustomViewProperties, } from '@umbraco-cms/backoffice/block-custom-view'; import type { UmbExtensionElementInitializer } from '@umbraco-cms/backoffice/extension-api'; -import { UUIBlinkAnimationValue, UUIBlinkKeyframes } from '@umbraco-cms/backoffice/external/uui'; -import { UMB_PROPERTY_CONTEXT, UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; -import { UMB_CLIPBOARD_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/clipboard'; +import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; import '../ref-list-block/index.js'; import '../inline-list-block/index.js'; import '../unsupported-list-block/index.js'; + /** * @element umb-block-list-entry */ @@ -309,7 +310,7 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper const workspaceName = propertyDatasetContext?.getName(); const propertyLabel = propertyContext?.getLabel(); - const blockLabel = this._label; + const blockLabel = this.#context.getName(); const entryName = workspaceName ? `${workspaceName} - ${propertyLabel} - ${blockLabel}` diff --git a/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts index c5c98d5f1a..9f67b19f5c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts @@ -1,14 +1,15 @@ import { UmbClipboardCollectionRepository } from '../../collection/index.js'; import type { UmbClipboardEntryDetailModel } from '../types.js'; -import { html, customElement, state, repeat, property } from '@umbraco-cms/backoffice/external/lit'; -import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; -import { UmbEntityContext, type UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; -import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import { css, customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { UmbEntityContext } from '@umbraco-cms/backoffice/entity'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbRequestReloadChildrenOfEntityEvent, UmbRequestReloadStructureForEntityEvent, } from '@umbraco-cms/backoffice/entity-action'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; // TODO: make this into an extension point (Picker) with two kinds of pickers: tree-item-picker and collection-item-picker; @customElement('umb-clipboard-entry-picker') @@ -117,19 +118,24 @@ export class UmbClipboardEntryPickerElement extends UmbLitElement { }; override render() { - return html`${this._items.length > 0 - ? repeat( + return when( + this._items.length > 0, + () => + repeat( this._items, (item) => item.unique, (item) => this.#renderItem(item), - ) - : html`There are no items in the clipboard`}`; + ), + () => html`

There are no items in the clipboard.

`, + ); } #renderItem(item: UmbClipboardEntryDetailModel) { + const label = item.name ?? item.unique; return html` this.#selectionManager.select(item.unique)} @deselected=${() => this.#selectionManager.deselect(item.unique)} @@ -141,7 +147,7 @@ export class UmbClipboardEntryPickerElement extends UmbLitElement { #renderItemIcon(item: UmbClipboardEntryDetailModel) { const iconName = item.icon ?? 'icon-clipboard-entry'; - return html``; + return html``; } #renderItemActions(item: UmbClipboardEntryDetailModel) { @@ -168,6 +174,14 @@ export class UmbClipboardEntryPickerElement extends UmbLitElement { super.destroy(); } + + static override styles = [ + css` + :host { + --uui-menu-item-flat-structure: 1; + } + `, + ]; } declare global { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts index 115dd08c87..93dd29e1c8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts @@ -553,6 +553,7 @@ export type DatabaseSettingsPresentationModel = { serverPlaceholder: string; requiresCredentials: boolean; supportsIntegratedAuthentication: boolean; + supportsTrustServerCertificate: boolean; requiresConnectionTest: boolean; }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/manifests.ts index c1d88ac009..17760f4434 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/manifests.ts @@ -271,4 +271,14 @@ export const manifests: Array = [ }, js: () => import('../../../assets/lang/zh-tw.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.VI', + weight: 100, + name: 'Vietnamese Backoffice UI Localization', + meta: { + culture: 'vi', + }, + js: () => import('../../../assets/lang/vi.js'), + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts index 092f9d72b7..a4c4dcf504 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts @@ -14,6 +14,7 @@ import { manifests as dropdownManifests } from './dropdown/manifests.js'; import { manifests as eyeDropperManifests } from './eye-dropper/manifests.js'; import { manifests as iconPickerManifests } from './icon-picker/manifests.js'; import { manifests as labelManifests } from './label/manifests.js'; +import { manifests as missingManifests } from './missing/manifests.js'; import { manifests as multipleTextStringManifests } from './multiple-text-string/manifests.js'; import { manifests as numberManifests } from './number/manifests.js'; import { manifests as radioButtonListManifests } from './radio-button-list/manifests.js'; @@ -32,6 +33,7 @@ export const manifests: Array = [ ...eyeDropperManifests, ...iconPickerManifests, ...labelManifests, + ...missingManifests, ...multipleTextStringManifests, ...numberManifests, ...radioButtonListManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/manifests.ts new file mode 100644 index 0000000000..0575dfc63a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/manifests.ts @@ -0,0 +1,18 @@ +import { manifests as modalManifests } from './modal/manifests.js'; + +export const manifests: Array = [ + { + type: 'propertyEditorUi', + alias: 'Umb.PropertyEditorUi.Missing', + name: 'Missing Property Editor UI', + element: () => import('./property-editor-ui-missing.element.js'), + meta: { + label: 'Missing', + propertyEditorSchemaAlias: undefined, // By setting it to undefined, this editor won't appear in the property editor UI picker modal. + icon: 'icon-ordered-list', + group: '', + supportsReadOnly: true, + }, + }, + ...modalManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/constants.ts new file mode 100644 index 0000000000..fb0853adfa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/constants.ts @@ -0,0 +1 @@ +export * from './missing-editor-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/manifests.ts new file mode 100644 index 0000000000..3ef10f367f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/manifests.ts @@ -0,0 +1,8 @@ +export const manifests: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.MissingPropertyEditor', + name: 'Missing Property Editor Modal', + element: () => import('./missing-editor-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.element.ts new file mode 100644 index 0000000000..f71d9769aa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.element.ts @@ -0,0 +1,47 @@ +import type { UmbMissingPropertyModalData, UmbMissingPropertyModalResult } from './missing-editor-modal.token.js'; +import { html, customElement, css } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-missing-property-editor-modal') +export class UmbMissingPropertyEditorModalElement extends UmbModalBaseElement< + UmbMissingPropertyModalData, + UmbMissingPropertyModalResult +> { + override render() { + return html` + + + ${this.data?.value} + + + `; + } + + static override styles = [ + UmbTextStyles, + css` + uui-dialog-layout { + max-inline-size: 60ch; + } + #codeblock { + max-height: 300px; + overflow: auto; + } + `, + ]; +} + +export { UmbMissingPropertyEditorModalElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-missing-property-editor-modal': UmbMissingPropertyEditorModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.token.ts new file mode 100644 index 0000000000..9792759058 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.token.ts @@ -0,0 +1,17 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbMissingPropertyModalData { + value: string | undefined; +} + +export type UmbMissingPropertyModalResult = undefined; + +export const UMB_MISSING_PROPERTY_EDITOR_MODAL = new UmbModalToken< + UmbMissingPropertyModalData, + UmbMissingPropertyModalResult +>('Umb.Modal.MissingPropertyEditor', { + modal: { + type: 'dialog', + size: 'small', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing.element.ts new file mode 100644 index 0000000000..5ec66cbf83 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing.element.ts @@ -0,0 +1,56 @@ +import { UMB_MISSING_PROPERTY_EDITOR_MODAL } from './modal/missing-editor-modal.token.js'; +import { customElement, html } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; +import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; + +/** + * @element umb-property-editor-ui-missing + */ +@customElement('umb-property-editor-ui-missing') +export class UmbPropertyEditorUIMissingElement + extends UmbFormControlMixin(UmbLitElement, undefined) + implements UmbPropertyEditorUiElement +{ + constructor() { + super(); + + this.addValidator( + 'customError', + () => this.localize.term('errors_propertyHasErrors'), + () => true, + ); + + this.pristine = false; + } + + async #onDetails(event: Event) { + event.stopPropagation(); + + await umbOpenModal(this, UMB_MISSING_PROPERTY_EDITOR_MODAL, { + data: { + // If the value is an object, we stringify it to make sure we can display it properly. + // If it's a primitive value, we just convert it to string. + value: typeof this.value === 'object' ? JSON.stringify(this.value, null, 2) : String(this.value), + }, + }).catch(() => undefined); + } + + override render() { + return html` + `; + } +} + +export default UmbPropertyEditorUIMissingElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-editor-ui-missing': UmbPropertyEditorUIMissingElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/ufm/controllers/ufm-virtual-render.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/ufm/controllers/ufm-virtual-render.controller.ts index 840be8b92d..1c25519887 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/ufm/controllers/ufm-virtual-render.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/ufm/controllers/ufm-virtual-render.controller.ts @@ -12,21 +12,26 @@ export class UmbUfmVirtualRenderController extends UmbControllerBase { const items: Array = []; - items.push(element.shadowRoot?.textContent ?? element.textContent ?? ''); - - if (element.shadowRoot !== null) { - Array.from(element.shadowRoot.children).forEach((element) => { - items.push(this.#getTextFromDescendants(element)); - }); + // Try get the text content from the shadow root first, otherwise get it from the light DOM. [LK] + if (element.shadowRoot) { + for (const node of element.shadowRoot.childNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + items.push(this.#getTextFromDescendants(node as Element)); + } else if (node.nodeType === Node.TEXT_NODE) { + items.push(node.textContent ?? ''); + } + } + } else { + for (const node of element.childNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + items.push(this.#getTextFromDescendants(node as Element)); + } else if (node.nodeType === Node.TEXT_NODE) { + items.push(node.textContent ?? ''); + } + } } - if (element.children !== null) { - Array.from(element.children).forEach((element) => { - items.push(this.#getTextFromDescendants(element)); - }); - } - - return items.filter((x) => x).join(' '); + return items.filter((x) => x).join(''); } set markdown(markdown: string | undefined) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishContentTypeFactoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishContentTypeFactoryTest.cs index 025281e566..aa736641c7 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishContentTypeFactoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishContentTypeFactoryTest.cs @@ -81,6 +81,9 @@ internal sealed class PublishContentTypeFactoryTest : UmbracoIntegrationTest { var dataType = new DataTypeBuilder() .WithId(0) + .AddEditor() + .WithAlias(Constants.PropertyEditors.Aliases.TextBox) + .Done() .Build(); dataType.EditorUiAlias = "NotUpdated"; var dataTypeCreateResult = await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index b007634a01..e7f01226fa 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -690,7 +690,7 @@ internal sealed class ContentServiceTests : UmbracoIntegrationTestWithContent } [Test] - public void Can_Get_Scheduled_Content_Keys() + public void Can_Get_Content_Schedules_By_Keys() { // Arrange var root = ContentService.GetById(Textpage.Id); @@ -701,11 +701,12 @@ internal sealed class ContentServiceTests : UmbracoIntegrationTestWithContent ContentService.Publish(content, content.AvailableCultures.ToArray()); // Act - var keys = ContentService.GetScheduledContentKeys([Textpage.Key, Subpage.Key, Subpage2.Key]).ToList(); + var keys = ContentService.GetContentSchedulesByIds([Textpage.Key, Subpage.Key, Subpage2.Key]).ToList(); // Assert Assert.AreEqual(1, keys.Count); - Assert.AreEqual(Subpage.Key, keys.First()); + Assert.AreEqual(keys[0].Key, Subpage.Id); + Assert.AreEqual(keys[0].Value.First().Id, contentSchedule.FullSchedule.First().Id); } [Test] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaEditingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaEditingServiceTests.cs index bc21bc29b6..1d4fb84e64 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaEditingServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaEditingServiceTests.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Nodes; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -19,29 +20,41 @@ internal sealed class MediaEditingServiceTests : UmbracoIntegrationTest protected IMediaType ImageMediaType { get; set; } + protected IMediaType ArticleMediaType { get; set; } + [SetUp] public async Task Setup() { ImageMediaType = MediaTypeService.Get(Constants.Conventions.MediaTypes.Image); + ArticleMediaType = MediaTypeService.Get(Constants.Conventions.MediaTypes.ArticleAlias); } [Test] - public async Task Cannot_Create_Media_With_Mandatory_Property() + public async Task Cannot_Create_Media_With_Mandatory_File_Property_Without_Providing_File() { - var imageModel = CreateMediaCreateModel("Image", new Guid(), ImageMediaType.Key); + var imageModel = CreateMediaCreateModel("Image", Guid.NewGuid(), ImageMediaType.Key); var imageCreateAttempt = await MediaEditingService.CreateAsync(imageModel, Constants.Security.SuperUserKey); - // Assert Assert.IsFalse(imageCreateAttempt.Success); Assert.AreEqual(ContentEditingOperationStatus.PropertyValidationError, imageCreateAttempt.Status); } [Test] - public async Task Can_Create_Media_Without_Mandatory_Property() + public async Task Can_Create_Media_With_Mandatory_File_Property_With_File_Provided() { - ImageMediaType.PropertyTypes.First(x => x.Alias == "umbracoFile").Mandatory = false; + var imageModel = CreateMediaCreateModelWithFile("Image", Guid.NewGuid(), ArticleMediaType.Key); + var imageCreateAttempt = await MediaEditingService.CreateAsync(imageModel, Constants.Security.SuperUserKey); + + Assert.IsTrue(imageCreateAttempt.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, imageCreateAttempt.Status); + } + + [Test] + public async Task Can_Create_Media_With_Optional_File_Property_Without_Providing_File() + { + ImageMediaType.PropertyTypes.First(x => x.Alias == Constants.Conventions.Media.File).Mandatory = false; MediaTypeService.Save(ImageMediaType); - var imageModel = CreateMediaCreateModel("Image", new Guid(), ImageMediaType.Key); + var imageModel = CreateMediaCreateModel("Image", Guid.NewGuid(), ImageMediaType.Key); var imageCreateAttempt = await MediaEditingService.CreateAsync(imageModel, Constants.Security.SuperUserKey); // Assert @@ -49,12 +62,57 @@ internal sealed class MediaEditingServiceTests : UmbracoIntegrationTest Assert.AreEqual(ContentEditingOperationStatus.Success, imageCreateAttempt.Status); } - private MediaCreateModel CreateMediaCreateModel(string name, Guid key, Guid mediaTypeKey) + [Test] + public async Task Can_Update_Media_With_Mandatory_File_Property_With_File_Provided() + { + Guid articleKey = Guid.NewGuid(); + var articleModel = CreateMediaCreateModelWithFile("Article", articleKey, ArticleMediaType.Key); + var articleCreateAttempt = await MediaEditingService.CreateAsync(articleModel, Constants.Security.SuperUserKey); + + Assert.IsTrue(articleCreateAttempt.Success); + + var updateModel = new MediaUpdateModel + { + Properties = + [ + new PropertyValueModel + { + Alias = Constants.Conventions.Media.File, + Value = new JsonObject + { + { "src", string.Empty }, + }, + } + ], + Variants = articleModel.Variants, + }; + var articleUpdateAttempt = await MediaEditingService.ValidateUpdateAsync(articleKey, updateModel); + Assert.IsFalse(articleUpdateAttempt.Success); + Assert.AreEqual(ContentEditingOperationStatus.PropertyValidationError, articleUpdateAttempt.Status); + } + + private static MediaCreateModel CreateMediaCreateModel(string name, Guid key, Guid mediaTypeKey) => new() { ContentTypeKey = mediaTypeKey, ParentKey = Constants.System.RootKey, - Variants = [new () { Name = name }], + Variants = [new() { Name = name }], Key = key, }; + + private static MediaCreateModel CreateMediaCreateModelWithFile(string name, Guid key, Guid mediaTypeKey) + { + var model = CreateMediaCreateModel(name, key, mediaTypeKey); + model.Properties = [ + new PropertyValueModel + { + Alias = Constants.Conventions.Media.File, + Value = new JsonObject + { + { "src", "reference-to-file" }, + }, + } + ]; + return model; + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index b49e9a7cc7..a396a7628a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -103,7 +104,14 @@ internal sealed class DocumentRepositoryTest : UmbracoIntegrationTest var ctRepository = CreateRepository(scopeAccessor, out contentTypeRepository, out TemplateRepository tr); var editors = new PropertyEditorCollection(new DataEditorCollection(() => Enumerable.Empty())); - dtdRepository = new DataTypeRepository(scopeAccessor, appCaches, editors, LoggerFactory.CreateLogger(), LoggerFactory, ConfigurationEditorJsonSerializer); + dtdRepository = new DataTypeRepository( + scopeAccessor, + appCaches, + editors, + LoggerFactory.CreateLogger(), + LoggerFactory, + ConfigurationEditorJsonSerializer, + Services.GetRequiredService()); return ctRepository; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs index 6870f301c5..675f52d8bc 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Linq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -487,4 +486,36 @@ internal sealed class DataTypeServiceTests : UmbracoIntegrationTest Assert.AreEqual("bodyText", secondResult.NodeAlias); Assert.AreEqual("Body text", secondResult.NodeName); } + + [Test] + public async Task Gets_MissingPropertyEditor_When_Editor_NotFound() + { + // Arrange + IDataType? dataType = (await DataTypeService.CreateAsync( + new DataType(new TestEditor(DataValueEditorFactory), ConfigurationEditorJsonSerializer) + { + Name = "Test Missing Editor", + DatabaseType = ValueStorageType.Ntext, + }, + Constants.Security.SuperUserKey)).Result; + + Assert.IsNotNull(dataType); + + // Act + IDataType? actual = await DataTypeService.GetAsync(dataType.Key); + + // Assert + Assert.NotNull(actual); + Assert.AreEqual(dataType.Key, actual.Key); + Assert.IsAssignableFrom(typeof(MissingPropertyEditor), actual.Editor); + Assert.AreEqual("Test Editor", actual.EditorAlias, "The alias should be the same as the original editor"); + Assert.AreEqual("Umb.PropertyEditorUi.Missing", actual.EditorUiAlias, "The editor UI alias should be the Missing Editor UI"); + } + + private class TestEditor : DataEditor + { + public TestEditor(IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) => + Alias = "Test Editor"; + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RelationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RelationServiceTests.cs index 42f6d15e69..f74503ff28 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RelationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RelationServiceTests.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Relations; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; -using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; @@ -32,6 +31,13 @@ internal sealed class RelationServiceTests : UmbracoIntegrationTest private IRelationService RelationService => GetRequiredService(); + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + base.CustomTestSetup(builder); + builder + .AddNotificationHandler(); + } + [Test] public void Get_Paged_Relations_By_Relation_Type() { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackRelationsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackRelationsTests.cs index ee05d4fef5..c748b9781a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackRelationsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackRelationsTests.cs @@ -3,7 +3,9 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Relations; using Umbraco.Cms.Tests.Common.Attributes; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; @@ -29,11 +31,12 @@ internal sealed class TrackRelationsTests : UmbracoIntegrationTestWithContent private IRelationService RelationService => GetRequiredService(); - // protected override void CustomTestSetup(IUmbracoBuilder builder) - // { - // base.CustomTestSetup(builder); - // builder.AddNuCache(); - // } + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + base.CustomTestSetup(builder); + builder + .AddNotificationHandler(); + } [Test] [LongRunning] @@ -89,6 +92,5 @@ internal sealed class TrackRelationsTests : UmbracoIntegrationTestWithContent Assert.AreEqual(c1.Id, relations[2].ChildId); Assert.AreEqual(Constants.Conventions.RelationTypes.RelatedMemberAlias, relations[3].RelationType.Alias); Assert.AreEqual(member.Id, relations[3].ChildId); - } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackedReferencesServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackedReferencesServiceTests.cs index 3e74f5ccb5..ae868d00fc 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackedReferencesServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackedReferencesServiceTests.cs @@ -3,7 +3,9 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Relations; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Common.Testing; @@ -25,6 +27,13 @@ internal class TrackedReferencesServiceTests : UmbracoIntegrationTest private IContentType ContentType { get; set; } + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + base.CustomTestSetup(builder); + builder + .AddNotificationHandler(); + } + [SetUp] public void Setup() => CreateTestData(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProviderTests.cs index 7bc7435919..58dcb07c32 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProviderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProviderTests.cs @@ -11,116 +11,76 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Management.Services.Signs; internal class HasPendingChangesSignProviderTests { [Test] - public void HasPendingChangesSignProvider_Can_Provide_Document_Tree_Signs() + public void HasPendingChangesSignProvider_Can_Provide_Variant_Item_Signs() { var sut = new HasPendingChangesSignProvider(); - Assert.IsTrue(sut.CanProvideSigns()); + Assert.IsTrue(sut.CanProvideSigns()); } [Test] - public void HasPendingChangesSignProvider_Can_Provide_Document_Collection_Signs() + public void HasPendingChangesSignProvider_Can_Provide_Variant_Signs() { var sut = new HasPendingChangesSignProvider(); - Assert.IsTrue(sut.CanProvideSigns()); + Assert.IsTrue(sut.CanProvideSigns()); } [Test] - public void HasPendingChangesSignProvider_Can_Provide_Document_Item_Signs() - { - var sut = new HasPendingChangesSignProvider(); - Assert.IsTrue(sut.CanProvideSigns()); - } - - [Test] - public async Task HasPendingChangesSignProvider_Should_Populate_Document_Tree_Signs() + public async Task HasPendingChangesSignProvider_Should_Populate_Variant_Item_Signs() { var sut = new HasPendingChangesSignProvider(); - var viewModels = new List + var variants = new List { - new() { Id = Guid.NewGuid() }, new() { - Id = Guid.NewGuid(), Variants = - [ - new() - { - State = DocumentVariantState.PublishedPendingChanges, - Culture = null, - Name = "Test", - }, - ], + State = DocumentVariantState.PublishedPendingChanges, + Culture = null, + Name = "Test", + }, + new() + { + State = DocumentVariantState.Published, + Culture = null, + Name = "Test2", }, }; - await sut.PopulateSignsAsync(viewModels); + await sut.PopulateSignsAsync(variants); - Assert.AreEqual(viewModels[0].Signs.Count(), 0); - Assert.AreEqual(viewModels[1].Signs.Count(), 1); + Assert.AreEqual(variants[0].Signs.Count(), 1); + Assert.AreEqual(variants[1].Signs.Count(), 0); - var signModel = viewModels[1].Signs.First(); + var signModel = variants[0].Signs.First(); Assert.AreEqual("Umb.PendingChanges", signModel.Alias); } [Test] - public async Task HasPendingChangesSignProvider_Should_Populate_Document_Collection_Signs() + public async Task HasPendingChangesSignProvider_Should_Populate_Variant_Signs() { var sut = new HasPendingChangesSignProvider(); - var viewModels = new List + var variants = new List { - new() { Id = Guid.NewGuid() }, new() { - Id = Guid.NewGuid(), Variants = - [ - new() - { - State = DocumentVariantState.PublishedPendingChanges, - Culture = null, - Name = "Test", - }, - ], + State = DocumentVariantState.PublishedPendingChanges, + Culture = null, + Name = "Test", + }, + new() + { + State = DocumentVariantState.Published, + Culture = null, + Name = "Test2", }, }; - await sut.PopulateSignsAsync(viewModels); + await sut.PopulateSignsAsync(variants); - Assert.AreEqual(viewModels[0].Signs.Count(), 0); - Assert.AreEqual(viewModels[1].Signs.Count(), 1); + Assert.AreEqual(variants[0].Signs.Count(), 1); + Assert.AreEqual(variants[1].Signs.Count(), 0); - var signModel = viewModels[1].Signs.First(); - Assert.AreEqual("Umb.PendingChanges", signModel.Alias); - } - - [Test] - public async Task HasPendingChangesSignProvider_Should_Populate_Document_Item_Signs() - { - var sut = new HasPendingChangesSignProvider(); - - var viewModels = new List - { - new() { Id = Guid.NewGuid() }, - new() - { - Id = Guid.NewGuid(), Variants = - [ - new() - { - State = DocumentVariantState.PublishedPendingChanges, - Culture = null, - Name = "Test", - }, - ], - }, - }; - - await sut.PopulateSignsAsync(viewModels); - - Assert.AreEqual(viewModels[0].Signs.Count(), 0); - Assert.AreEqual(viewModels[1].Signs.Count(), 1); - - var signModel = viewModels[1].Signs.First(); + var signModel = variants[0].Signs.First(); Assert.AreEqual("Umb.PendingChanges", signModel.Alias); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProviderTests.cs index 48b97ff30b..7292b3a9ae 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProviderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProviderTests.cs @@ -1,9 +1,13 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Api.Management.Services.Signs; +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; using Umbraco.Cms.Api.Management.ViewModels.Document.Item; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; @@ -16,8 +20,9 @@ internal class HasScheduleSignProviderTests public void HasScheduleSignProvider_Can_Provide_Document_Tree_Signs() { var contentServiceMock = new Mock(); + var idKeyMapMock = new Mock(); - var sut = new HasScheduleSignProvider(contentServiceMock.Object); + var sut = new HasScheduleSignProvider(contentServiceMock.Object, idKeyMapMock.Object); Assert.IsTrue(sut.CanProvideSigns()); } @@ -25,8 +30,9 @@ internal class HasScheduleSignProviderTests public void HasScheduleSignProvider_Can_Provide_Document_Collection_Signs() { var contentServiceMock = new Mock(); + var idKeyMapMock = new Mock(); - var sut = new HasScheduleSignProvider(contentServiceMock.Object); + var sut = new HasScheduleSignProvider(contentServiceMock.Object, idKeyMapMock.Object); Assert.IsTrue(sut.CanProvideSigns()); } @@ -34,8 +40,9 @@ internal class HasScheduleSignProviderTests public void HasScheduleSignProvider_Can_Provide_Document_Item_Signs() { var contentServiceMock = new Mock(); + var idKeyMapMock = new Mock(); - var sut = new HasScheduleSignProvider(contentServiceMock.Object); + var sut = new HasScheduleSignProvider(contentServiceMock.Object, idKeyMapMock.Object); Assert.IsTrue(sut.CanProvideSigns()); } @@ -47,23 +54,37 @@ internal class HasScheduleSignProviderTests new() { Key = Guid.NewGuid(), Name = "Item 1" }, new() { Key = Guid.NewGuid(), Name = "Item 2" }, }; + var idKeyMapMock = new Mock(); + idKeyMapMock.Setup(x => x.GetIdForKey(entities[0].Key, UmbracoObjectTypes.Document)) + .Returns(Attempt.Succeed(1)); + idKeyMapMock.Setup(x => x.GetIdForKey(entities[1].Key, UmbracoObjectTypes.Document)) + .Returns(Attempt.Succeed(2)); + + Guid[] keys = entities.Select(x => x.Key).ToArray(); var contentServiceMock = new Mock(); contentServiceMock - .Setup(x => x.GetScheduledContentKeys(It.IsAny>())) - .Returns([entities[1].Key]); - var sut = new HasScheduleSignProvider(contentServiceMock.Object); + .Setup(x => x.GetContentSchedulesByIds(keys)) + .Returns(CreateContentSchedules()); + + + var sut = new HasScheduleSignProvider(contentServiceMock.Object, idKeyMapMock.Object); + + var variant1 = new DocumentVariantItemResponseModel() { State = DocumentVariantState.Published, Name = "Test1", Culture = "en-EN" }; + var variant2 = new DocumentVariantItemResponseModel() { State = DocumentVariantState.Published, Name = "Test1", Culture = "da-DA" }; + var variant3 = new DocumentVariantItemResponseModel() { State = DocumentVariantState.PublishedPendingChanges, Name = "Test" }; var viewModels = new List { - new() { Id = entities[0].Key }, new() { Id = entities[1].Key }, + new() { Id = entities[0].Key, Variants = [variant1, variant2] }, new() { Id = entities[1].Key, Variants = [variant3] }, }; await sut.PopulateSignsAsync(viewModels); - Assert.AreEqual(viewModels[0].Signs.Count(), 0); - Assert.AreEqual(viewModels[1].Signs.Count(), 1); + Assert.AreEqual(viewModels[0].Variants.FirstOrDefault(x => x.Culture == "da-DA").Signs.Count(), 0); + Assert.AreEqual(viewModels[0].Variants.FirstOrDefault(x => x.Culture == "en-EN").Signs.Count(), 1); + Assert.AreEqual(viewModels[1].Variants.First().Signs.Count(), 1); - var signModel = viewModels[1].Signs.First(); + var signModel = viewModels[0].Variants.First().Signs.First(); Assert.AreEqual("Umb.ScheduledForPublish", signModel.Alias); } @@ -75,23 +96,36 @@ internal class HasScheduleSignProviderTests new() { Key = Guid.NewGuid(), Name = "Item 1" }, new() { Key = Guid.NewGuid(), Name = "Item 2" }, }; + var idKeyMapMock = new Mock(); + idKeyMapMock.Setup(x => x.GetIdForKey(entities[0].Key, UmbracoObjectTypes.Document)) + .Returns(Attempt.Succeed(1)); + idKeyMapMock.Setup(x => x.GetIdForKey(entities[1].Key, UmbracoObjectTypes.Document)) + .Returns(Attempt.Succeed(2)); + + Guid[] keys = entities.Select(x => x.Key).ToArray(); var contentServiceMock = new Mock(); contentServiceMock - .Setup(x => x.GetScheduledContentKeys(It.IsAny>())) - .Returns([entities[1].Key]); - var sut = new HasScheduleSignProvider(contentServiceMock.Object); + .Setup(x => x.GetContentSchedulesByIds(keys)) + .Returns(CreateContentSchedules()); + + var sut = new HasScheduleSignProvider(contentServiceMock.Object, idKeyMapMock.Object); + + var variant1 = new DocumentVariantResponseModel() { State = DocumentVariantState.Published, Name = "Test1", Culture = "en-EN" }; + var variant2 = new DocumentVariantResponseModel() { State = DocumentVariantState.Published, Name = "Test1", Culture = "da-DA" }; + var variant3 = new DocumentVariantResponseModel() { State = DocumentVariantState.PublishedPendingChanges, Name = "Test" }; var viewModels = new List { - new() { Id = entities[0].Key }, new() { Id = entities[1].Key }, + new() { Id = entities[0].Key, Variants = [variant1, variant2] }, new() { Id = entities[1].Key, Variants = [variant3] }, }; await sut.PopulateSignsAsync(viewModels); - Assert.AreEqual(viewModels[0].Signs.Count(), 0); - Assert.AreEqual(viewModels[1].Signs.Count(), 1); + Assert.AreEqual(viewModels[0].Variants.FirstOrDefault(x => x.Culture == "da-DA").Signs.Count(), 0); + Assert.AreEqual(viewModels[0].Variants.FirstOrDefault(x => x.Culture == "en-EN").Signs.Count(), 1); + Assert.AreEqual(viewModels[1].Variants.First().Signs.Count(), 1); - var signModel = viewModels[1].Signs.First(); + var signModel = viewModels[0].Variants.First().Signs.First(); Assert.AreEqual("Umb.ScheduledForPublish", signModel.Alias); } @@ -103,23 +137,51 @@ internal class HasScheduleSignProviderTests new() { Key = Guid.NewGuid(), Name = "Item 1" }, new() { Key = Guid.NewGuid(), Name = "Item 2" }, }; + var idKeyMapMock = new Mock(); + idKeyMapMock.Setup(x => x.GetIdForKey(entities[0].Key, UmbracoObjectTypes.Document)) + .Returns(Attempt.Succeed(1)); + idKeyMapMock.Setup(x => x.GetIdForKey(entities[1].Key, UmbracoObjectTypes.Document)) + .Returns(Attempt.Succeed(2)); + + Guid[] keys = entities.Select(x => x.Key).ToArray(); var contentServiceMock = new Mock(); contentServiceMock - .Setup(x => x.GetScheduledContentKeys(It.IsAny>())) - .Returns([entities[1].Key]); - var sut = new HasScheduleSignProvider(contentServiceMock.Object); + .Setup(x => x.GetContentSchedulesByIds(keys)) + .Returns(CreateContentSchedules()); + + var sut = new HasScheduleSignProvider(contentServiceMock.Object, idKeyMapMock.Object); + + var variant1 = new DocumentVariantItemResponseModel() { State = DocumentVariantState.Published, Name = "Test1", Culture = "en-EN" }; + var variant2 = new DocumentVariantItemResponseModel() { State = DocumentVariantState.Published, Name = "Test1", Culture = "da-DA" }; + var variant3 = new DocumentVariantItemResponseModel() { State = DocumentVariantState.PublishedPendingChanges, Name = "Test" }; var viewModels = new List { - new() { Id = entities[0].Key }, new() { Id = entities[1].Key }, + new() { Id = entities[0].Key, Variants = [variant1, variant2] }, new() { Id = entities[1].Key, Variants = [variant3] }, }; await sut.PopulateSignsAsync(viewModels); - Assert.AreEqual(viewModels[0].Signs.Count(), 0); - Assert.AreEqual(viewModels[1].Signs.Count(), 1); + Assert.AreEqual(viewModels[0].Variants.FirstOrDefault(x => x.Culture == "da-DA").Signs.Count(), 0); + Assert.AreEqual(viewModels[0].Variants.FirstOrDefault(x => x.Culture == "en-EN").Signs.Count(), 1); + Assert.AreEqual(viewModels[1].Variants.First().Signs.Count(), 1); - var signModel = viewModels[1].Signs.First(); + var signModel = viewModels[0].Variants.First().Signs.First(); Assert.AreEqual("Umb.ScheduledForPublish", signModel.Alias); } + + private Dictionary> CreateContentSchedules() + { + Dictionary> contentSchedules = new Dictionary>(); + + contentSchedules.Add(1, [ + new ContentSchedule("en-EN", DateTime.Now.AddDays(1), ContentScheduleAction.Release), // Scheduled for release + new ContentSchedule("da-DA", DateTime.Now.AddDays(-1), ContentScheduleAction.Release) // Not Scheduled for release + ]); + contentSchedules.Add(2, [ + new ContentSchedule("*", DateTime.Now.AddDays(1), ContentScheduleAction.Release) // Scheduled for release + ]); + + return contentSchedules; + } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/FileUploadValueRequiredValidatorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/FileUploadValueRequiredValidatorTests.cs new file mode 100644 index 0000000000..a2910f6970 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/FileUploadValueRequiredValidatorTests.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Nodes; +using NUnit.Framework; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Infrastructure.PropertyEditors.Validators; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors.Validators; + +[TestFixture] +public class FileUploadValueRequiredValidatorTests +{ + [Test] + public void Validates_Empty_File_Upload_As_Not_Provided() + { + var validator = new FileUploadValueRequiredValidator(); + + var value = JsonNode.Parse("{ \"src\": \"\", \"settingsData\": [] }"); + var result = validator.ValidateRequired(value, ValueTypes.Json); + Assert.AreEqual(1, result.Count()); + } + + [Test] + public void Valdiates_File_Upload_As_Provided() + { + var validator = new FileUploadValueRequiredValidator(); + + var value = JsonNode.Parse("{ \"src\": \"fakePath\", \"settingsData\": [] }"); + var result = validator.ValidateRequired(value, ValueTypes.Json); + Assert.IsEmpty(result); + } +}