Added dictionary import/export (#12378)

* Added dictionary import/export

* Added umb tree to dictionary import & level for displaying preview dictionary import

* Indented dictionaries for import, added new text for choosing where to import dictionary items

* Removed console.log for dictionary/import.controller.js

Co-authored-by: Michael <michael@crossingpaths.be>
This commit is contained in:
Johannes Lantz
2022-06-17 10:21:01 +02:00
committed by GitHub
parent 2f4feb925a
commit d42a695e8a
12 changed files with 461 additions and 2 deletions

View File

@@ -36,6 +36,7 @@
<key alias="restore" version="7.3.0">Restore</key>
<key alias="chooseWhereToCopy">Choose where to copy</key>
<key alias="chooseWhereToMove">Choose where to move</key>
<key alias="chooseWhereToImport">Choose where to import</key>
<key alias="toInTheTreeStructureBelow">to in the tree structure below</key>
<key alias="infiniteEditorChooseWhereToCopy">Choose where to copy the selected item(s)</key>
<key alias="infiniteEditorChooseWhereToMove">Choose where to move the selected item(s)</key>
@@ -569,9 +570,14 @@
<key alias="deletingALayout">Modifying layout will result in loss of data for any existing content that is based on this configuration.</key>
</area>
<area alias="dictionary">
<key alias="importDictionaryItemHelp">
To import a dictionary item, find the ".udt" file on your computer by clicking the
"Import" button (you'll be asked for confirmation on the next screen)
</key>
<key alias="itemDoesNotExists">Dictionary item does not exist.</key>
<key alias="parentDoesNotExists">Parent item does not exist.</key>
<key alias="noItems">There are no dictionary items.</key>
<key alias="noItemsInFile">There are no dictionary items in this file.</key>
<key alias="createNew">Create dictionary item</key>
</area>
<area alias="dictionaryItem">
@@ -1592,6 +1598,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="resendInviteSuccess">Invitation has been re-sent to %0%</key>
<key alias="documentTypeExportedSuccess">Document Type was exported to file</key>
<key alias="documentTypeExportedError">An error occurred while exporting the Document Type</key>
<key alias="dictionaryItemExportedSuccess">Dictionary item(s) was exported to file</key>
<key alias="dictionaryItemExportedError">An error occurred while exporting the dictionary item(s)</key>
<key alias="dictionaryItemImported">The following dictionary item(s) has been imported!</key>
<key alias="publishWithNoDomains">Domains are not configured for multilingual site, please contact an administrator,
see log for more information
</key>

View File

@@ -37,6 +37,7 @@
<key alias="restore" version="7.3.0">Restore</key>
<key alias="chooseWhereToCopy">Choose where to copy</key>
<key alias="chooseWhereToMove">Choose where to move</key>
<key alias="chooseWhereToImport">Choose where to import</key>
<key alias="toInTheTreeStructureBelow">to in the tree structure below</key>
<key alias="infiniteEditorChooseWhereToCopy">Choose where to copy the selected item(s)</key>
<key alias="infiniteEditorChooseWhereToMove">Choose where to move the selected item(s)</key>
@@ -579,9 +580,14 @@
<key alias="deletingALayout">Modifying layout will result in loss of data for any existing content that is based on this configuration.</key>
</area>
<area alias="dictionary">
<key alias="importDictionaryItemHelp">
To import a dictionary item, find the ".udt" file on your computer by clicking the
"Import" button (you'll be asked for confirmation on the next screen)
</key>
<key alias="itemDoesNotExists">Dictionary item does not exist.</key>
<key alias="parentDoesNotExists">Parent item does not exist.</key>
<key alias="noItems">There are no dictionary items.</key>
<key alias="noItemsInFile">There are no dictionary items in this file.</key>
<key alias="createNew">Create dictionary item</key>
</area>
<area alias="dictionaryItem">
@@ -1629,6 +1635,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="contentCultureValidationError">Validation failed for language '%0%'</key>
<key alias="documentTypeExportedSuccess">Document Type was exported to file</key>
<key alias="documentTypeExportedError">An error occurred while exporting the Document Type</key>
<key alias="dictionaryItemExportedSuccess">Dictionary item(s) was exported to file</key>
<key alias="dictionaryItemExportedError">An error occurred while exporting the dictionary item(s)</key>
<key alias="dictionaryItemImported">The following dictionary item(s) has been imported!</key>
<key alias="scheduleErrReleaseDate1">The release date cannot be in the past</key>
<key alias="scheduleErrReleaseDate2">Cannot schedule the document for publishing since the required '%0%' is not
published

View File

@@ -0,0 +1,14 @@
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Models
{
[DataContract(Name = "dictionaryImportModel")]
public class DictionaryImportModel
{
[DataMember(Name = "dictionaryItems")]
public List<DictionaryPreviewImportModel>? DictionaryItems { get; set; }
[DataMember(Name = "tempFileName")]
public string? TempFileName { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Models
{
[DataContract(Name = "dictionaryPreviewImportModel")]
public class DictionaryPreviewImportModel
{
[DataMember(Name = "name")]
public string? Name { get; set; }
[DataMember(Name = "level")]
public int Level { get; set; }
}
}

View File

@@ -1355,6 +1355,12 @@ namespace Umbraco.Cms.Infrastructure.Packaging
return ImportDictionaryItems(dictionaryItemElementList, languages, null, userId);
}
public IEnumerable<IDictionaryItem> ImportDictionaryItem(XElement dictionaryItemElement, int userId, Guid? parentId)
{
var languages = _localizationService.GetAllLanguages().ToList();
return ImportDictionaryItem(dictionaryItemElement, languages, parentId, userId);
}
private IReadOnlyList<IDictionaryItem> ImportDictionaryItems(IEnumerable<XElement> dictionaryItemElementList,
List<ILanguage> languages, Guid? parentId, int userId)
{

View File

@@ -1,4 +1,5 @@
using System;
using System.Xml;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -14,12 +15,16 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
using Umbraco.Cms.Infrastructure.Packaging;
using Constants = Umbraco.Cms.Core.Constants;
using System.Xml.Linq;
using Microsoft.AspNetCore.Http;
namespace Umbraco.Cms.Web.BackOffice.Controllers
{
@@ -42,6 +47,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
private readonly GlobalSettings _globalSettings;
private readonly ILocalizedTextService _localizedTextService;
private readonly IUmbracoMapper _umbracoMapper;
private readonly IEntityXmlSerializer _serializer;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly PackageDataInstallation _packageDataInstallation;
public DictionaryController(
ILogger<DictionaryController> logger,
@@ -49,7 +57,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IOptionsSnapshot<GlobalSettings> globalSettings,
ILocalizedTextService localizedTextService,
IUmbracoMapper umbracoMapper
IUmbracoMapper umbracoMapper,
IEntityXmlSerializer serializer,
IHostingEnvironment hostingEnvironment,
PackageDataInstallation packageDataInstallation
)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -58,6 +69,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
_globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings));
_localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService));
_umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper));
_serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
_hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
_packageDataInstallation = packageDataInstallation ?? throw new ArgumentNullException(nameof(packageDataInstallation));
}
/// <summary>
@@ -354,6 +368,126 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
}
}
public IActionResult ExportDictionary(int id, bool includeChildren = false)
{
var dictionaryItem = _localizationService.GetDictionaryItemById(id);
if (dictionaryItem == null)
throw new NullReferenceException("No dictionary item found with id " + id);
var xml = _serializer.Serialize(dictionaryItem, includeChildren);
var fileName = $"{dictionaryItem.ItemKey}.udt";
// Set custom header so umbRequestHelper.downloadFile can save the correct filename
HttpContext.Response.Headers.Add("x-filename", fileName);
return File(Encoding.UTF8.GetBytes(xml.ToDataString()), MediaTypeNames.Application.Octet, fileName);
}
public IActionResult ImportDictionary(string file, int parentId)
{
if (string.IsNullOrWhiteSpace(file))
return NotFound();
var filePath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), file);
if (!System.IO.File.Exists(filePath))
return NotFound();
var xd = new XmlDocument { XmlResolver = null };
xd.Load(filePath);
var userId = _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? 0;
var element = XElement.Parse(xd.InnerXml);
var parentDictionaryItem = _localizationService.GetDictionaryItemById(parentId);
var dictionaryItems = _packageDataInstallation.ImportDictionaryItem(element, userId, parentDictionaryItem?.Key);
// Try to clean up the temporary file.
try
{
System.IO.File.Delete(filePath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error cleaning up temporary udt file in {File}", filePath);
}
var model = _umbracoMapper.Map<IDictionaryItem, DictionaryDisplay>(dictionaryItems.FirstOrDefault());
return Content(model!.Path, MediaTypeNames.Text.Plain, Encoding.UTF8);
}
public ActionResult<DictionaryImportModel> Upload(IFormFile file)
{
if (file == null)
return ValidationProblem(
_localizedTextService.Localize("media", "failedFileUpload"),
_localizedTextService.Localize("speechBubbles", "fileErrorNotFound"));
var fileName = file.FileName.Trim(Constants.CharArrays.DoubleQuote);
var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower();
var root = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads);
var tempPath = Path.Combine(root, fileName);
if (!Path.GetFullPath(tempPath).StartsWith(Path.GetFullPath(root)))
return ValidationProblem(
_localizedTextService.Localize("media", "failedFileUpload"),
_localizedTextService.Localize("media", "invalidFileName"));
if (!ext.InvariantEquals("udt"))
return ValidationProblem(
_localizedTextService.Localize("media", "failedFileUpload"),
_localizedTextService.Localize("media", "disallowedFileType"));
using (var stream = System.IO.File.Create(tempPath))
{
file.CopyToAsync(stream).GetAwaiter().GetResult();
}
var xd = new XmlDocument
{
XmlResolver = null
};
xd.Load(tempPath);
if (xd.DocumentElement == null)
return ValidationProblem(
_localizedTextService.Localize("media", "failedFileUpload"),
_localizedTextService.Localize("speechBubbles", "fileErrorNotFound"));
DictionaryImportModel model = new DictionaryImportModel()
{
TempFileName = tempPath,
DictionaryItems = new List<DictionaryPreviewImportModel>()
};
int level = 1;
string curentParrent = string.Empty;
foreach (XmlNode dictionaryItem in xd.GetElementsByTagName("DictionaryItem"))
{
var name = dictionaryItem.Attributes?.GetNamedItem("Name")?.Value ?? string.Empty;
var parentKey = dictionaryItem?.ParentNode?.Attributes?.GetNamedItem("Key")?.Value ?? string.Empty;
if (parentKey != curentParrent || level == 1)
{
level += 1;
curentParrent = parentKey;
}
model.DictionaryItems.Add(new DictionaryPreviewImportModel()
{
Level = level,
Name = name
});
}
if (!model.DictionaryItems.Any())
return ValidationProblem(
_localizedTextService.Localize("media", "failedFileUpload"),
_localizedTextService.Localize("dictionary", "noItemsInFile"));
return model;
}
private static Func<IDictionaryItem, string> ItemSort() => item => item.ItemKey;
}
}

View File

@@ -130,8 +130,25 @@ namespace Umbraco.Cms.Web.BackOffice.Trees
if (id != Constants.System.RootString)
{
menu.Items.Add<ActionDelete>(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false);
menu.Items.Add<ActionMove>(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false);
menu.Items.Add(new MenuItem("export", LocalizedTextService)
{
Icon = "icon-download-alt",
SeparatorBefore = true,
OpensDialog = true,
UseLegacyIcon = false
});
menu.Items.Add<ActionDelete>(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false);
}
else
{
menu.Items.Add(new MenuItem("import", LocalizedTextService)
{
Icon = "icon-page-up",
SeparatorBefore = true,
OpensDialog = true,
UseLegacyIcon = false
});
}
menu.Items.Add(new RefreshNode(LocalizedTextService, true));

View File

@@ -160,6 +160,77 @@ function dictionaryResource($q, $http, $location, umbRequestHelper, umbDataForma
"Failed to save data for dictionary id " + dictionary.id);
}
/**
* @ngdoc method
* @name umbraco.resources.dictionaryResource#export
* @methodOf umbraco.resources.dictionaryResource
*
* @description
* Export dictionary items of a given id.
*
* ##usage
* <pre>
* dictionaryResource.exportItem(1234){
* .then(function() {
* Do stuff..
* });
* </pre>
*
* @param {Int} id the ID of the dictionary item so export
* @param {Bool?} includeChildren if children should also be exported
* @returns {Promise} resourcePromise object.
*
*/
function exportItem(id, includeChildren) {
if (!id) {
throw "id cannot be null";
}
var url = umbRequestHelper.getApiUrl("dictionaryApiBaseUrl", "ExportDictionary", { id: id, includeChildren: includeChildren });
return umbRequestHelper.downloadFile(url).then(function () {
localizationService.localize("speechBubbles_dictionaryItemExportedSuccess").then(function(value) {
notificationsService.success(value);
});
}, function (data) {
localizationService.localize("speechBubbles_dictionaryItemExportedError").then(function(value) {
notificationsService.error(value);
});
});
}
/**
* @ngdoc method
* @name umbraco.resources.dictionaryResource#import
* @methodOf umbraco.resources.dictionaryResource
*
* @description
* Import a dictionary item from a file
*
* ##usage
* <pre>
* dictionaryResource.importItem("path to file"){
* .then(function() {
* Do stuff..
* });
* </pre>
*
* @param {String} file path of the file to import
* @param {Int?} parentId the int of the parent dictionary item to move incomming dictionary items to
* @returns {Promise} resourcePromise object.
*
*/
function importItem(file, parentId) {
if (!file) {
throw "file cannot be null";
}
return umbRequestHelper.resourcePromise(
$http.post(umbRequestHelper.getApiUrl("dictionaryApiBaseUrl", "ImportDictionary", { file: file, parentId: parentId })),
"Failed to import dictionary item " + file
);
}
/**
* @ngdoc method
* @name umbraco.resources.dictionaryResource#getList
@@ -194,6 +265,8 @@ function dictionaryResource($q, $http, $location, umbRequestHelper, umbDataForma
getById: getById,
save: save,
move: move,
exportItem: exportItem,
importItem: importItem,
getList : getList
};

View File

@@ -0,0 +1,18 @@
angular.module("umbraco")
.controller("Umbraco.Editors.Dictionary.ExportController",
function ($scope, dictionaryResource, navigationService) {
$scope.includeChildren = false;
$scope.toggleHandler = function () {
$scope.includeChildren = !$scope.includeChildren
};
$scope.export = function () {
dictionaryResource.exportItem($scope.currentNode.id, $scope.includeChildren);
navigationService.hideMenu();
};
$scope.cancel = function () {
navigationService.hideDialog();
};
});

View File

@@ -0,0 +1,17 @@
<div class="umb-dialog umb-pane" ng-controller="Umbraco.Editors.Dictionary.ExportController">
<div class="umb-dialog-body form-horizontal" ng-cloak>
<umb-pane>
<umb-control-group localize="label" label="@defaultdialogs_includeDescendants">
<umb-toggle checked="includeChildren" on-click="toggleHandler()"></umb-toggle>
</umb-control-group>
</umb-pane>
</div>
<div class="umb-dialog-footer btn-toolbar umb-btn-toolbar">
<button type="button" class="btn btn-link" ng-click="cancel()">
<localize key="general_cancel">Cancel</localize>
</button>
<button class="btn btn-primary" ng-click="export()">
<localize key="actions_export">Export</localize>
</button>
</div>
</div>

View File

@@ -0,0 +1,67 @@
angular.module("umbraco")
.controller("Umbraco.Editors.Dictionary.ImportController",
function ($scope, dictionaryResource, notificationsService, navigationService, Upload, umbRequestHelper) {
var vm = this;
vm.state = "upload";
vm.model = {};
vm.uploadStatus = "";
vm.cancelButtonLabel = "cancel";
$scope.dialogTreeApi = {};
function nodeSelectHandler(args) {
args.event.preventDefault();
args.event.stopPropagation();
if ($scope.target) {
//un-select if there's a current one selected
$scope.target.selected = false;
}
$scope.target = args.node;
$scope.target.selected = true;
}
$scope.handleFiles = function (files, event, invalidFiles) {
if (files && files.length > 0) {
$scope.upload(files[0]);
}
};
$scope.upload = function (file) {
Upload.upload({
url: umbRequestHelper.getApiUrl("dictionaryApiBaseUrl", "Upload"),
fields: {},
file: file
}).then(function (response) {
vm.model = response.data;
vm.state = "confirm";
vm.uploadStatus = "done";
}, function (err) {
notificationsService.error(err.data.notifications[0].header, err.data.notifications[0].message);
});
};
$scope.import = function () {
var parentId = $scope.target !== undefined ? $scope.target.id : 0;
dictionaryResource.importItem(vm.model.tempFileName, parentId).then(function (path) {
navigationService.syncTree({ tree: "dictionary", path: path, forceReload: true, activate: false });
vm.state = "done";
vm.cancelButtonLabel = "general_close";
});
};
$scope.onTreeInit = function () {
$scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler);
};
$scope.close = function () {
navigationService.hideDialog();
};
});

View File

@@ -0,0 +1,81 @@
<div class="umb-dialog" ng-controller="Umbraco.Editors.Dictionary.ImportController as vm">
<div class="umb-dialog-body with-footer">
<div class="umb-pane">
<div ng-if="vm.state === 'upload'">
<p>
<localize key="dictionary_importDictionaryItemHelp">
To import a Dictionary item, find the '.udt' file on your computer by clicking the 'Import' button (you'll be
asked for confirmation on the next screen)
</localize>
</p>
<form name="importDictionaryItem">
<!-- Select files -->
<button accept=".udt"
class="btn btn-action"
name="file"
ng-model="filesHolder"
ngf-change="handleFiles($files, $event, $invalidFiles)"
ngf-multiple="true"
ngf-pattern="*.udt"
ngf-select>
<localize key="general_import">Import</localize>
</button>
</form>
</div>
<div ng-if="vm.state === 'confirm'">
<strong>
<localize key="dictionaryListCaption">Dictionary items</localize>
:
</strong>
<div ng-repeat="dictionaryItem in vm.model.dictionaryItems" style="padding-left:{{dictionaryItem.level * 20}}px;">{{dictionaryItem.name}}</div>
<br />
<br />
<div>
<hr />
<p>
<strong>
<localize key="actions_chooseWhereToImport">Chose where to import</localize>
<localize key="dictionaryListCaption">dictionary items</localize>.
</strong>
(optinal)
</p>
<umb-tree section="translation"
treealias="dictionary"
customtreeparams="foldersonly=1"
hideheader="false"
hideoptions="true"
isdialog="true"
api="dialogTreeApi"
on-init="onTreeInit()"
enablecheckboxes="true">
</umb-tree>
</div>
<button class="btn btn-primary" ng-click="import()">
<localize key="general_import">Import</localize>
</button>
</div>
<div ng-if="vm.state === 'done'">
<strong>
<localize key="speechBubbles_dictionaryItemImported">The following dictionary item(s) has been imported!</localize>
</strong>
<div ng-repeat="dictionaryItem in vm.model.dictionaryItems" style="padding-left:{{dictionaryItem.level * 20}}px;">{{dictionaryItem.name}}</div>
</div>
</div>
</div>
<div class="umb-dialog-footer btn-toolbar umb-btn-toolbar">
<umb-button action="close()"
button-style="link"
label-key="{{vm.cancelButtonLabel}}"
type="button">
</umb-button>
</div>
</div>