Starts implementing ability to manually run pending migrations from the back office
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Cms.Core.Manifest
|
||||
{
|
||||
public interface IManifestParser
|
||||
@@ -14,5 +16,11 @@ namespace Umbraco.Cms.Core.Manifest
|
||||
/// Parses a manifest.
|
||||
/// </summary>
|
||||
PackageManifest ParseManifest(string text);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all package individual manifests
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IEnumerable<PackageManifest> GetManifests();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
@@ -10,6 +10,19 @@ namespace Umbraco.Cms.Core.Manifest
|
||||
[DataContract]
|
||||
public class PackageManifest
|
||||
{
|
||||
[DataMember(Name = "name", IsRequired = true)]
|
||||
public string PackageName { get; set; }
|
||||
|
||||
[DataMember(Name = "packageView", IsRequired = true)]
|
||||
public string PackageView { get; set; }
|
||||
|
||||
// TODO: iconUrl? since we cannot retrieve from nuget
|
||||
|
||||
// TODO: Version since we cannot retrieve from nuget
|
||||
|
||||
//[DataMember(Name = "name", IsRequired = true)]
|
||||
//public Guid PackageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source path of the manifest.
|
||||
/// </summary>
|
||||
|
||||
27
src/Umbraco.Core/Packaging/InstalledPackage.cs
Normal file
27
src/Umbraco.Core/Packaging/InstalledPackage.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Core.Packaging
|
||||
{
|
||||
[DataContract(Name = "installedPackage")]
|
||||
public class InstalledPackage
|
||||
{
|
||||
[DataMember(Name = "name", IsRequired = true)]
|
||||
[Required]
|
||||
public string PackageName { get; set; }
|
||||
|
||||
// TODO: Version? Icon? Other metadata?
|
||||
|
||||
[DataMember(Name = "packageView")]
|
||||
public string PackageView { get; set; }
|
||||
|
||||
[DataMember(Name = "plans")]
|
||||
public IEnumerable<InstalledPackageMigrationPlans> PackageMigrationPlans { get; set; } = Enumerable.Empty<InstalledPackageMigrationPlans>();
|
||||
|
||||
[DataMember(Name = "hasPendingMigrations")]
|
||||
public bool HasPendingMigrations => PackageMigrationPlans.Any(x => x.HasPendingMigrations);
|
||||
}
|
||||
|
||||
}
|
||||
27
src/Umbraco.Core/Packaging/InstalledPackageMigrationPlans.cs
Normal file
27
src/Umbraco.Core/Packaging/InstalledPackageMigrationPlans.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Core.Packaging
|
||||
{
|
||||
[DataContract(Name = "installedPackageMigrations")]
|
||||
public class InstalledPackageMigrationPlans
|
||||
{
|
||||
[DataMember(Name = "hasPendingMigrations")]
|
||||
public bool HasPendingMigrations => FinalMigrationId != CurrentMigrationId;
|
||||
|
||||
/// <summary>
|
||||
/// If the package has migrations, this will be it's final migration Id
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This can be used to determine if the package advertises any migrations
|
||||
/// </remarks>
|
||||
[DataMember(Name = "finalMigrationId")]
|
||||
public string FinalMigrationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the package has migrations, this will be it's current migration Id
|
||||
/// </summary>
|
||||
[DataMember(Name = "currentMigrationId")]
|
||||
public string CurrentMigrationId { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,7 +7,13 @@ using Umbraco.Cms.Core.Models.Packaging;
|
||||
|
||||
namespace Umbraco.Cms.Core.Packaging
|
||||
{
|
||||
// This is the thing that goes in the createdPackages.config
|
||||
|
||||
/// <summary>
|
||||
/// A created package in the back office.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This data structure is persisted to createdPackages.config when creating packages in the back office.
|
||||
/// </remarks>
|
||||
[DataContract(Name = "packageInstance")]
|
||||
public class PackageDefinition
|
||||
{
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
using System;
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
using Umbraco.Cms.Core.Migrations;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Packaging
|
||||
{
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Base class for package migration plans
|
||||
/// </summary>
|
||||
public abstract class PackageMigrationPlan : MigrationPlan, IDiscoverable
|
||||
{
|
||||
protected PackageMigrationPlan(string name) : base(name)
|
||||
/// <summary>
|
||||
/// Creates a package migration plan
|
||||
/// </summary>
|
||||
/// <param name="packageName">The name of the package. If the package has a package.manifest these must match.</param>
|
||||
protected PackageMigrationPlan(string packageName) : this(packageName, packageName)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a plan for a Package Name
|
||||
/// </summary>
|
||||
/// <param name="packageName">The package name that the plan is for. If the package has a package.manifest these must match.</param>
|
||||
/// <param name="planName">
|
||||
/// The plan name for the package. This should be the same name as the
|
||||
/// package name if there is only one plan in the package.
|
||||
/// </param>
|
||||
protected PackageMigrationPlan(string packageName, string planName) : base(planName)
|
||||
{
|
||||
// A call to From must be done first
|
||||
From(string.Empty);
|
||||
|
||||
DefinePlan();
|
||||
PackageName = packageName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -25,6 +41,11 @@ namespace Umbraco.Cms.Core.Packaging
|
||||
/// </summary>
|
||||
public override bool IgnoreCurrentState => true;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Package Name for this plan
|
||||
/// </summary>
|
||||
public string PackageName { get; }
|
||||
|
||||
protected abstract void DefinePlan();
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Umbraco.Cms.Core.Packaging
|
||||
/// These are the key/value pairs from the keyvalue storage of migration names and their final values
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public IReadOnlyList<string> GetUmbracoPendingPackageMigrations(IReadOnlyDictionary<string, string> keyValues)
|
||||
public IReadOnlyList<string> GetPendingPackageMigrations(IReadOnlyDictionary<string, string> keyValues)
|
||||
{
|
||||
var packageMigrationPlans = _packageMigrationPlans.ToList();
|
||||
|
||||
|
||||
@@ -24,10 +24,25 @@ namespace Umbraco.Cms.Core.Services
|
||||
|
||||
InstallationSummary InstallCompiledPackageData(XDocument packageXml, int userId = Constants.Security.SuperUserId);
|
||||
|
||||
IEnumerable<PackageDefinition> GetAllInstalledPackages();
|
||||
/// <summary>
|
||||
/// Returns the advertised installed packages
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IEnumerable<InstalledPackage> GetAllInstalledPackages();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the created packages
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IEnumerable<PackageDefinition> GetAllCreatedPackages();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a created package by id
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
PackageDefinition GetCreatedPackageById(int id);
|
||||
|
||||
void DeleteCreatedPackage(int id, int userId = Constants.Security.SuperUserId);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Umbraco.Cms.Core.Manifest
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private readonly IDataValueEditorFactory _dataValueEditorFactory;
|
||||
private static readonly string Utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble());
|
||||
private static readonly string s_utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble());
|
||||
|
||||
private readonly IAppPolicyCache _cache;
|
||||
private readonly ILogger<ManifestParser> _logger;
|
||||
@@ -97,7 +97,7 @@ namespace Umbraco.Cms.Core.Manifest
|
||||
/// <summary>
|
||||
/// Gets all manifests.
|
||||
/// </summary>
|
||||
private IEnumerable<PackageManifest> GetManifests()
|
||||
public IEnumerable<PackageManifest> GetManifests()
|
||||
{
|
||||
var manifests = new List<PackageManifest>();
|
||||
|
||||
@@ -108,8 +108,11 @@ namespace Umbraco.Cms.Core.Manifest
|
||||
var text = File.ReadAllText(path);
|
||||
text = TrimPreamble(text);
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
continue;
|
||||
var manifest = ParseManifest(text);
|
||||
}
|
||||
|
||||
PackageManifest manifest = ParseManifest(text);
|
||||
manifest.Source = path;
|
||||
manifests.Add(manifest);
|
||||
}
|
||||
@@ -174,8 +177,8 @@ namespace Umbraco.Cms.Core.Manifest
|
||||
private static string TrimPreamble(string text)
|
||||
{
|
||||
// strangely StartsWith(preamble) would always return true
|
||||
if (text.Substring(0, 1) == Utf8Preamble)
|
||||
text = text.Remove(0, Utf8Preamble.Length);
|
||||
if (text.Substring(0, 1) == s_utf8Preamble)
|
||||
text = text.Remove(0, s_utf8Preamble.Length);
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime
|
||||
return UmbracoDatabaseState.NeedsUpgrade;
|
||||
}
|
||||
|
||||
IReadOnlyList<string> packagesRequiringMigration = _packageMigrationState.GetUmbracoPendingPackageMigrations(keyValues);
|
||||
IReadOnlyList<string> packagesRequiringMigration = _packageMigrationState.GetPendingPackageMigrations(keyValues);
|
||||
if (packagesRequiringMigration.Count > 0)
|
||||
{
|
||||
_startupState[PendingPacakgeMigrationsStateKey] = packagesRequiringMigration;
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Manifest;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Packaging;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
@@ -19,6 +20,10 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
private readonly IPackageInstallation _packageInstallation;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IManifestParser _manifestParser;
|
||||
private readonly IKeyValueService _keyValueService;
|
||||
private readonly PackageMigrationPlanCollection _packageMigrationPlans;
|
||||
private readonly PendingPackageMigrations _pendingPackageMigrations;
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly ICreatedPackagesRepository _createdPackages;
|
||||
|
||||
@@ -26,12 +31,20 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
IAuditService auditService,
|
||||
ICreatedPackagesRepository createdPackages,
|
||||
IPackageInstallation packageInstallation,
|
||||
IEventAggregator eventAggregator)
|
||||
IEventAggregator eventAggregator,
|
||||
IManifestParser manifestParser,
|
||||
IKeyValueService keyValueService,
|
||||
PackageMigrationPlanCollection packageMigrationPlans,
|
||||
PendingPackageMigrations pendingPackageMigrations)
|
||||
{
|
||||
_auditService = auditService;
|
||||
_createdPackages = createdPackages;
|
||||
_packageInstallation = packageInstallation;
|
||||
_eventAggregator = eventAggregator;
|
||||
_manifestParser = manifestParser;
|
||||
_keyValueService = keyValueService;
|
||||
_packageMigrationPlans = packageMigrationPlans;
|
||||
_pendingPackageMigrations = pendingPackageMigrations;
|
||||
}
|
||||
|
||||
#region Installation
|
||||
@@ -95,10 +108,52 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
|
||||
public string ExportCreatedPackage(PackageDefinition definition) => _createdPackages.ExportPackage(definition);
|
||||
|
||||
public IEnumerable<InstalledPackage> GetAllInstalledPackages()
|
||||
{
|
||||
IReadOnlyDictionary<string, string> keyValues = _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix);
|
||||
IReadOnlyList<string> pendingMigrations = _pendingPackageMigrations.GetPendingPackageMigrations(keyValues);
|
||||
|
||||
var installedPackages = new Dictionary<string, InstalledPackage>();
|
||||
|
||||
foreach(PackageMigrationPlan plan in _packageMigrationPlans)
|
||||
{
|
||||
if (!installedPackages.TryGetValue(plan.PackageName, out InstalledPackage installedPackage))
|
||||
{
|
||||
installedPackage = new InstalledPackage
|
||||
{
|
||||
PackageName = plan.PackageName
|
||||
};
|
||||
installedPackages.Add(plan.PackageName, installedPackage);
|
||||
}
|
||||
|
||||
var currentPlans = installedPackage.PackageMigrationPlans.ToList();
|
||||
keyValues.TryGetValue(Constants.Conventions.Migrations.KeyValuePrefix + plan.PackageName, out var currentState);
|
||||
currentPlans.Add(new InstalledPackageMigrationPlans
|
||||
{
|
||||
CurrentMigrationId = currentState,
|
||||
FinalMigrationId = plan.FinalState
|
||||
});
|
||||
|
||||
installedPackage.PackageMigrationPlans = currentPlans;
|
||||
}
|
||||
|
||||
foreach(PackageManifest package in _manifestParser.GetManifests())
|
||||
{
|
||||
if (!installedPackages.TryGetValue(package.PackageName, out InstalledPackage installedPackage))
|
||||
{
|
||||
installedPackage = new InstalledPackage
|
||||
{
|
||||
PackageName = package.PackageName
|
||||
};
|
||||
installedPackages.Add(package.PackageName, installedPackage);
|
||||
}
|
||||
|
||||
installedPackage.PackageView = package.PackageView;
|
||||
}
|
||||
|
||||
return installedPackages.Values;
|
||||
}
|
||||
|
||||
// TODO: Implement
|
||||
public IEnumerable<PackageDefinition> GetAllInstalledPackages() => Enumerable.Empty<PackageDefinition>();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Packaging
|
||||
{
|
||||
private static readonly Guid s_step1 = Guid.NewGuid();
|
||||
private static readonly Guid s_step2 = Guid.NewGuid();
|
||||
private const string PackageName = "Test1";
|
||||
private const string TestPackageName = "Test1";
|
||||
|
||||
private class TestPackageMigrationPlan : PackageMigrationPlan
|
||||
{
|
||||
public TestPackageMigrationPlan() : base(PackageName)
|
||||
public TestPackageMigrationPlan() : base(TestPackageName)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Packaging
|
||||
{
|
||||
PendingPackageMigrations pendingPackageMigrations = GetPendingPackageMigrations();
|
||||
var registeredMigrations = new Dictionary<string, string>();
|
||||
IReadOnlyList<string> pending = pendingPackageMigrations.GetUmbracoPendingPackageMigrations(registeredMigrations);
|
||||
IReadOnlyList<string> pending = pendingPackageMigrations.GetPendingPackageMigrations(registeredMigrations);
|
||||
Assert.AreEqual(1, pending.Count);
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Packaging
|
||||
PendingPackageMigrations pendingPackageMigrations = GetPendingPackageMigrations();
|
||||
var registeredMigrations = new Dictionary<string, string>
|
||||
{
|
||||
[Constants.Conventions.Migrations.KeyValuePrefix + PackageName] = s_step2.ToString()
|
||||
[Constants.Conventions.Migrations.KeyValuePrefix + TestPackageName] = s_step2.ToString()
|
||||
};
|
||||
IReadOnlyList<string> pending = pendingPackageMigrations.GetUmbracoPendingPackageMigrations(registeredMigrations);
|
||||
IReadOnlyList<string> pending = pendingPackageMigrations.GetPendingPackageMigrations(registeredMigrations);
|
||||
Assert.AreEqual(0, pending.Count);
|
||||
}
|
||||
|
||||
@@ -63,9 +63,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Packaging
|
||||
PendingPackageMigrations pendingPackageMigrations = GetPendingPackageMigrations();
|
||||
var registeredMigrations = new Dictionary<string, string>
|
||||
{
|
||||
[Constants.Conventions.Migrations.KeyValuePrefix + PackageName] = s_step1.ToString()
|
||||
[Constants.Conventions.Migrations.KeyValuePrefix + TestPackageName] = s_step1.ToString()
|
||||
};
|
||||
IReadOnlyList<string> pending = pendingPackageMigrations.GetUmbracoPendingPackageMigrations(registeredMigrations);
|
||||
IReadOnlyList<string> pending = pendingPackageMigrations.GetPendingPackageMigrations(registeredMigrations);
|
||||
Assert.AreEqual(1, pending.Count);
|
||||
}
|
||||
|
||||
@@ -75,9 +75,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Packaging
|
||||
PendingPackageMigrations pendingPackageMigrations = GetPendingPackageMigrations();
|
||||
var registeredMigrations = new Dictionary<string, string>
|
||||
{
|
||||
[Constants.Conventions.Migrations.KeyValuePrefix + PackageName] = s_step1.ToString().ToUpper()
|
||||
[Constants.Conventions.Migrations.KeyValuePrefix + TestPackageName] = s_step1.ToString().ToUpper()
|
||||
};
|
||||
IReadOnlyList<string> pending = pendingPackageMigrations.GetUmbracoPendingPackageMigrations(registeredMigrations);
|
||||
IReadOnlyList<string> pending = pendingPackageMigrations.GetPendingPackageMigrations(registeredMigrations);
|
||||
Assert.AreEqual(1, pending.Count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,10 +129,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
/// Returns all installed packages - only shows their latest versions
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<PackageDefinition> GetInstalled()
|
||||
{
|
||||
return _packagingService.GetAllInstalledPackages()
|
||||
.ToList();
|
||||
}
|
||||
public IEnumerable<InstalledPackage> GetInstalled()
|
||||
=> _packagingService.GetAllInstalledPackages().ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function PackagesInstalledController($scope, $route, $location, packageResource, $timeout, $window, localStorageService, localizationService) {
|
||||
function PackagesInstalledController($location, packageResource, localizationService) {
|
||||
|
||||
var vm = this;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr ng-repeat="installedPackage in vm.installedPackages track by installedPackage.id">
|
||||
<tr ng-repeat="installedPackage in vm.installedPackages track by installedPackage.name">
|
||||
<td class="flex items-center">
|
||||
<div class="umb-package-list__item-icon">
|
||||
<umb-icon ng-if="!installedPackage.iconUrl" icon="icon-box" class="icon-box"></umb-icon>
|
||||
@@ -17,25 +17,30 @@
|
||||
</div>
|
||||
<div class="umb-package-list__item-content">
|
||||
<div class="umb-package-list__item-name">{{ installedPackage.name }}</div>
|
||||
<div class="umb-package-list__item-description">
|
||||
{{ installedPackage.version }} | <a href="{{ installedPackage.url }}" target="_blank" rel="noopener">{{ installedPackage.url }}</a> | {{ installedPackage.author }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align: right;">
|
||||
<umb-button ng-show="installedPackage.packageView"
|
||||
type="button"
|
||||
button-style="info"
|
||||
size="xxs"
|
||||
label-key="packager_packageOptions"
|
||||
action="vm.packageOptions(installedPackage)">
|
||||
</umb-button>
|
||||
<umb-button ng-show="installedPackage.hasPendingMigrations"
|
||||
type="button"
|
||||
button-style="info"
|
||||
size="xxs"
|
||||
label-key="packager_packageMigrationsRun"
|
||||
action="vm.packageOptions(installedPackage)">
|
||||
</umb-button>
|
||||
|
||||
<umb-button type="button"
|
||||
button-style="danger"
|
||||
label-key="packager_packageUninstallHeader"
|
||||
action="vm.confirmUninstall(installedPackage)">
|
||||
</umb-button>
|
||||
<umb-button ng-show="installedPackage.packageView"
|
||||
type="button"
|
||||
button-style="info"
|
||||
size="xxs"
|
||||
label-key="packager_packageOptions"
|
||||
action="vm.packageOptions(installedPackage)">
|
||||
</umb-button>
|
||||
|
||||
<umb-button type="button"
|
||||
button-style="danger"
|
||||
label-key="packager_packageUninstallHeader"
|
||||
action="vm.confirmUninstall(installedPackage)">
|
||||
</umb-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -1264,6 +1264,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
<key alias="packageNoItemsText"><![CDATA[This package file doesn't contain any items to uninstall.<br/><br/>
|
||||
You can safely remove this from the system by clicking "uninstall package" below.]]></key>
|
||||
<key alias="packageOptions">Package options</key>
|
||||
<key alias="packageMigrationsRun">Run pending package migrations</key>
|
||||
<key alias="packageReadme">Package readme</key>
|
||||
<key alias="packageRepository">Package repository</key>
|
||||
<key alias="packageUninstallConfirm">Confirm package uninstall</key>
|
||||
|
||||
@@ -1276,6 +1276,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
<key alias="packageNoItemsText"><![CDATA[This package file doesn't contain any items to uninstall.<br/><br/>
|
||||
You can safely remove this from the system by clicking "uninstall package" below.]]></key>
|
||||
<key alias="packageOptions">Package options</key>
|
||||
<key alias="packageMigrationsRun">Run pending package migrations</key>
|
||||
<key alias="packageReadme">Package readme</key>
|
||||
<key alias="packageRepository">Package repository</key>
|
||||
<key alias="packageUninstallConfirm">Confirm package uninstall</key>
|
||||
|
||||
Reference in New Issue
Block a user