diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Language/LanguageViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Language/LanguageViewModelsMapDefinition.cs index 4fceb353fc8d..ce756fa0c426 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Language/LanguageViewModelsMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Language/LanguageViewModelsMapDefinition.cs @@ -21,6 +21,7 @@ private static void Map(ILanguage source, LanguageResponseModel target, MapperCo target.Name = source.CultureName; target.IsDefault = source.IsDefault; target.IsMandatory = source.IsMandatory; + target.SortOrder = source.SortOrder; } // Umbraco.Code.MapAll -Id -Key @@ -37,6 +38,7 @@ private static void Map(CreateLanguageRequestModel source, ILanguage target, Map target.IsoCode = source.IsoCode; target.UpdateDate = default; target.FallbackIsoCode = source.FallbackIsoCode; + target.SortOrder = source.SortOrder; } // Umbraco.Code.MapAll -Id -Key -IsoCode -CreateDate @@ -51,5 +53,6 @@ private static void Map(UpdateLanguageRequestModel source, ILanguage target, Map target.IsMandatory = source.IsMandatory; target.UpdateDate = default; target.FallbackIsoCode = source.FallbackIsoCode; + target.SortOrder = source.SortOrder; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Language/LanguageModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Language/LanguageModelBase.cs index db288316a19d..3f82a7861dcd 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Language/LanguageModelBase.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Language/LanguageModelBase.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Umbraco.Cms.Api.Management.ViewModels.Language; @@ -12,4 +12,6 @@ public class LanguageModelBase public bool IsMandatory { get; set; } public string? FallbackIsoCode { get; set; } + + public int SortOrder { get; set; } } diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs index 5af66089cad8..f09076a2bfb0 100644 --- a/src/Umbraco.Core/Models/ILanguage.cs +++ b/src/Umbraco.Core/Models/ILanguage.cs @@ -57,4 +57,12 @@ public interface ILanguage : IEntity, IRememberBeingDirty /// [DataMember] public string? FallbackIsoCode { get; set; } + + /// + /// Gets or sets the sort order. + /// + /// + /// The sort order. + /// + int SortOrder { get; set; } } diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index efed3314dfd7..1c202d8c40d7 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -16,6 +16,7 @@ public class Language : EntityBase, ILanguage private bool _isDefaultVariantLanguage; private string _isoCode; private bool _mandatory; + private int _sortOrder; /// /// Initializes a new instance of the class. @@ -28,14 +29,14 @@ public Language(string isoCode, string cultureName) _cultureName = cultureName ?? throw new ArgumentNullException(nameof(cultureName)); } - /// - [DataMember] - public string IsoCode + /// + [DataMember] + public string IsoCode + { + get => _isoCode; + set { - get => _isoCode; - set - { - ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(value); SetPropertyValueAndDetectChanges(value, ref _isoCode!, nameof(IsoCode)); } @@ -78,4 +79,12 @@ public string? FallbackIsoCode get => _fallbackLanguageIsoCode; set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageIsoCode, nameof(FallbackIsoCode)); } + + /// + [DataMember] + public int SortOrder + { + get => _sortOrder; + set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs index d9774aa1ea7a..637c1cc941a0 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs @@ -67,5 +67,6 @@ protected virtual void DefinePlan() To("{B9133686-B758-404D-AF12-708AA80C7E44}"); To("{EEB1F012-B44D-4AB4-8756-F7FB547345B4}"); To("{0F49E1A4-AFD8-4673-A91B-F64E78C48174}"); + To("{B1F2A6D3-4C8B-4E9F-8A5C-7D1B2E3F5A6B}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_1_0/AddSortOrderToLanguage.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_1_0/AddSortOrderToLanguage.cs new file mode 100644 index 000000000000..4f0b8e643011 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_1_0/AddSortOrderToLanguage.cs @@ -0,0 +1,145 @@ +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Scoping; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_16_1_0; + +public class AddSortOrderToLanguage : UnscopedMigrationBase +{ + private const string NewColumnName = "sortOrder"; + private readonly IScopeProvider _scopeProvider; + + public AddSortOrderToLanguage(IMigrationContext context, IScopeProvider scopeProvider) + : base(context) + => _scopeProvider = scopeProvider; + + protected override void Migrate() + { + // If the new column already exists we'll do nothing. + if (ColumnExists(Constants.DatabaseSchema.Tables.Language, NewColumnName)) + { + Context.Complete(); + return; + } + + using IScope scope = _scopeProvider.CreateScope(); + using IDisposable notificationSuppression = scope.Notifications.Suppress(); + ScopeDatabase(scope); + + // SQL server can simply add the column, but for SQLite this won't work, + // so we'll have to create a new table and copy over data. + if (DatabaseType != DatabaseType.SQLite) + { + MigrateSqlServer(); + } + else + { + MigrateSqlite(); + } + + Context.Complete(); + scope.Complete(); + } + + private void MigrateSqlServer() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + AddColumnIfNotExists(columns, NewColumnName); + } + + private void MigrateSqlite() + { + /* + * We commit the initial transaction started by the scope. This is required in order to disable the foreign keys. + * We then begin a new transaction, this transaction will be committed or rolled back by the scope, like normal. + * We don't have to worry about re-enabling the foreign keys, since these are enabled by default every time a connection is established. + * + * Ideally we'd want to do this with the unscoped database we get, however, this cannot be done, + * since our scoped database cannot share a connection with the unscoped database, so a new one will be created, which enables the foreign keys. + * Similarly we cannot use Database.CompleteTransaction(); since this also closes the connection, + * so starting a new transaction would re-enable foreign keys. + */ + Database.Execute("COMMIT;"); + Database.Execute("PRAGMA foreign_keys=off;"); + Database.Execute("BEGIN TRANSACTION;"); + + IEnumerable languages = Database.Fetch().Select(x => new LanguageDto + { + Id = x.Id, + IsoCode = x.IsoCode, + CultureName = x.CultureName, + IsDefault = x.IsDefault, + IsMandatory = x.IsMandatory, + FallbackLanguageId = x.FallbackLanguageId, + SortOrder = 0 + }); + + Delete.Table(Constants.DatabaseSchema.Tables.Language).Do(); + Create.Table().Do(); + + foreach (LanguageDto language in languages) + { + Database.Insert(Constants.DatabaseSchema.Tables.Language, "id", false, language); + } + } + + [TableName(TableName)] + [PrimaryKey("id")] + [ExplicitColumns] + internal class OldLanguageDto + { + public const string TableName = Constants.DatabaseSchema.Tables.Language; + + // Public constants to bind properties between DTOs + public const string IsoCodeColumnName = "languageISOCode"; + + /// + /// Gets or sets the identifier of the language. + /// + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 2)] + public short Id { get; set; } + + /// + /// Gets or sets the ISO code of the language. + /// + [Column(IsoCodeColumnName)] + [Index(IndexTypes.UniqueNonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(14)] + public string? IsoCode { get; set; } + + /// + /// Gets or sets the culture name of the language. + /// + [Column("languageCultureName")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(100)] + public string? CultureName { get; set; } + + /// + /// Gets or sets a value indicating whether the language is the default language. + /// + [Column("isDefaultVariantLang")] + [Constraint(Default = "0")] + public bool IsDefault { get; set; } + + /// + /// Gets or sets a value indicating whether the language is mandatory. + /// + [Column("mandatory")] + [Constraint(Default = "0")] + public bool IsMandatory { get; set; } + + /// + /// Gets or sets the identifier of a fallback language. + /// + [Column("fallbackLanguageId")] + [ForeignKey(typeof(LanguageDto), Column = "id")] + [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + public int? FallbackLanguageId { get; set; } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs index da5a8ad665fb..646f6f51d86e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs @@ -33,6 +33,9 @@ internal class DomainDto [ResultColumn("languageISOCode")] public string IsoCode { get; set; } = null!; + /// + /// Gets or sets the sort order of the domain. + /// [Column("sortOrder")] public int SortOrder { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs index 3fe65f83229f..60de6437932d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs @@ -60,4 +60,10 @@ internal class LanguageDto [Index(IndexTypes.NonClustered)] [NullSetting(NullSetting = NullSettings.Null)] public int? FallbackLanguageId { get; set; } + + /// + /// Gets or sets the sort order of the language. + /// + [Column("sortOrder")] + public int SortOrder { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs index 32b389296413..eb2ce18c929a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs @@ -21,7 +21,8 @@ public static ILanguage BuildEntity(LanguageDto dto, string? fallbackIsoCode) Id = dto.Id, IsDefault = dto.IsDefault, IsMandatory = dto.IsMandatory, - FallbackIsoCode = fallbackIsoCode + SortOrder = dto.SortOrder, + FallbackIsoCode = fallbackIsoCode, }; // Reset dirty initial properties @@ -40,6 +41,7 @@ public static LanguageDto BuildDto(ILanguage entity, int? fallbackLanguageId) CultureName = entity.CultureName, IsDefault = entity.IsDefault, IsMandatory = entity.IsMandatory, + SortOrder = entity.SortOrder, FallbackLanguageId = fallbackLanguageId }; diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs index ed5ef6b2244b..d8b185f37afd 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs @@ -21,5 +21,6 @@ protected override void DefineMaps() DefineMap(nameof(Language.Id), nameof(LanguageDto.Id)); DefineMap(nameof(Language.IsoCode), nameof(LanguageDto.IsoCode)); DefineMap(nameof(Language.CultureName), nameof(LanguageDto.CultureName)); + DefineMap(nameof(Language.SortOrder), nameof(LanguageDto.SortOrder)); } } 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 cf958d9ab757..6fe1146b61fc 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 @@ -1140,14 +1140,15 @@ export type ItemSortingRequestModel = { export type LanguageItemResponseModel = { name: string; - isoCode: string; + isoCode: string; }; export type LanguageResponseModel = { name: string; isDefault: boolean; isMandatory: boolean; - fallbackIsoCode?: string | null; + fallbackIsoCode?: string | null; + sortOrder: number; isoCode: string; }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-action/publish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-action/publish.action.ts index cba68820a77e..cf8354bd8b6d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-action/publish.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-action/publish.action.ts @@ -55,6 +55,7 @@ export class UmbPublishDocumentEntityAction extends UmbEntityActionBase { name: appCulture!, entityType: 'language', fallbackIsoCode: null, + sortOrder: 0, isDefault: true, isMandatory: false, unique: appCulture!, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-action/unpublish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-action/unpublish.action.ts index 667fb4dbebc1..a954e65b9fb7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-action/unpublish.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-action/unpublish.action.ts @@ -58,6 +58,7 @@ export class UmbUnpublishDocumentEntityAction extends UmbEntityActionBase name: appCulture!, entityType: 'language', fallbackIsoCode: null, + sortOrder: 0, isDefault: true, isMandatory: false, unique: appCulture!, diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/collection/repository/language-collection.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/language/collection/repository/language-collection.server.data-source.ts index 4130781beb23..0e337c142a28 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/collection/repository/language-collection.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/collection/repository/language-collection.server.data-source.ts @@ -41,6 +41,7 @@ export class UmbLanguageCollectionServerDataSource implements UmbCollectionDataS isDefault: item.isDefault, isMandatory: item.isMandatory, fallbackIsoCode: item.fallbackIsoCode || null, + sortOrder: item.sortOrder }; return model; diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts index b347db3e8196..6841735a620e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts @@ -38,6 +38,10 @@ export class UmbLanguageTableCollectionViewElement extends UmbLitElement { name: 'Fallback', alias: 'fallbackLanguage', }, + { + name: 'Sort Order', + alias: 'sortOrder', + }, { name: '', alias: 'entityActions', @@ -94,6 +98,10 @@ export class UmbLanguageTableCollectionViewElement extends UmbLitElement { columnAlias: 'fallbackLanguage', value: languages.find((x) => x.unique === language.fallbackIsoCode)?.name, }, + { + columnAlias: 'sortOrder', + value: language.sortOrder, + }, { columnAlias: 'entityActions', value: html` + + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/repository/detail/language-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/language/repository/detail/language-detail.server.data-source.ts index da721cf0b182..3e01b8627795 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/repository/detail/language-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/repository/detail/language-detail.server.data-source.ts @@ -38,6 +38,7 @@ export class UmbLanguageServerDataSource implements UmbDetailDataSource${this._language?.unique} + +
${this._language?.sortOrder}
+
+