diff --git a/Extensions/Signum.Files/FileSystemScope.cs b/Extensions/Signum.Files/FileSystemScope.cs new file mode 100644 index 0000000000..a0a096c775 --- /dev/null +++ b/Extensions/Signum.Files/FileSystemScope.cs @@ -0,0 +1,81 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Compression; + +namespace Signum.Files; + +public class FileSystemScope : IDisposable +{ + private static readonly Variable current = Statics.ThreadVariable("currentLimitedFileSystem"); + + private static RealLimitedFileSystem realFS = new(); + protected static ILimitedFileSystem Current => current.Value ?? realFS; + + public FileSystemScope(ILimitedFileSystem fs) + { + current.Value = fs; + } + + void IDisposable.Dispose() + { + current.Value = realFS; + } + + #region Directory + + public static class Directory + { + public static bool Exists(string path) => Current.DirectoryExists(path); + public static string[] GetFiles(string path) => Current.GetFiles(path, "*"); + public static string[] GetFiles(string path, string searchPattern) => Current.GetFiles(path, searchPattern); + public static DirectoryInfo CreateDirectory(string path) => Current.CreateDirectory(path); + public static DirectoryInfo[] GetDirectories(string path) => Current.GetDirectories(path); + public static void Delete(string path, bool recursive) => Current.DeleteDirectory(path, recursive); + + } + #endregion + + + #region File + + public static class File + { + public static Stream OpenWrite(string path) => Current.FileOpenWrite(path); + + public static void WriteAllBytes(string path, byte[] bytes) => Current.FileWriteAllBytes(path, bytes); + + public static byte[] ReadAllBytes(string path) => Current.FileReadAllBytes(path); + + public static Stream OpenRead(string path) => Current.FileOpenRead(path); + + public static void Delete(string path) => Current.FileDelete(path); + } + #endregion + + #region Path methods (Always call corresponding methods from System.IO.Paths) + + public static class Path + { + + // Path methods always call corresponding methods from System.IO.Path + + [return: NotNullIfNotNull(nameof(path))] + public static string? GetFileNameWithoutExtension(string? path) => System.IO.Path.GetFileNameWithoutExtension(path); + + [return: NotNullIfNotNull(nameof(path))] + public static string? GetFileName(string? path) => System.IO.Path.GetFileName(path); + + public static string Combine(string path1, string path2) => System.IO.Path.Combine(path1, path2); + + public static string Combine(string path1, string path2, string path3) => System.IO.Path.Combine(path1, path2, path3); + + public static string? GetDirectoryName(string? path) => System.IO.Path.GetDirectoryName(path); + + public static char[] GetInvalidPathChars() => System.IO.Path.GetInvalidPathChars(); + } + #endregion + +} + + + diff --git a/Extensions/Signum.Files/LimitedFileSystem.cs b/Extensions/Signum.Files/LimitedFileSystem.cs new file mode 100644 index 0000000000..89e09824bd --- /dev/null +++ b/Extensions/Signum.Files/LimitedFileSystem.cs @@ -0,0 +1,239 @@ +using System.IO; +using System.IO.Compression; +using System.Text.RegularExpressions; + +namespace Signum.Files; + +public interface ILimitedFileSystem +{ + bool DirectoryExists(string path); + DirectoryInfo CreateDirectory(string path); + DirectoryInfo[] GetDirectories(string path); + void DeleteDirectory(string path, bool recursive); + + string[] GetFiles(string path, string searchPattern); + Stream FileOpenWrite(string path); + void FileWriteAllBytes(string path, byte[] bytes); + byte[] FileReadAllBytes(string path); + Stream FileOpenRead(string path); + void FileDelete(string path); + +} + +public class RealLimitedFileSystem : ILimitedFileSystem +{ + bool ILimitedFileSystem.DirectoryExists(string path) => Directory.Exists(path); + + DirectoryInfo ILimitedFileSystem.CreateDirectory(string path) => Directory.CreateDirectory(path); + + DirectoryInfo[] ILimitedFileSystem.GetDirectories(string path) => new DirectoryInfo(path).GetDirectories(); + + void ILimitedFileSystem.DeleteDirectory(string path, bool recursive) => Directory.Delete(path, recursive); + + string[] ILimitedFileSystem.GetFiles(string path, string searchPattern) => Directory.GetFiles(path, searchPattern); + + Stream ILimitedFileSystem.FileOpenWrite(string path) => File.OpenWrite(path); + + void ILimitedFileSystem.FileWriteAllBytes(string path, byte[] bytes) => File.WriteAllBytes(path, bytes); + + byte[] ILimitedFileSystem.FileReadAllBytes(string path) => File.ReadAllBytes(path); + + Stream ILimitedFileSystem.FileOpenRead(string path) => File.OpenRead(path); + + void ILimitedFileSystem.FileDelete(string path) => File.Delete(path); + +} + +public abstract class ZipFileSystem +{ + protected readonly string root; //No trailing slash + + protected static string SlashFix(string path) => path.Replace(Path.DirectorySeparatorChar, '/'); + protected string Absolute(string path) => root.HasText() ? $"{root}/{path}" : path; + + public ZipFileSystem(string root) + { + this.root = root.HasText() ? SlashFix(root).TrimEnd('/', '\\') : ""; + } + + protected sealed class StreamWithCallback(Stream inner, Action onClose) : Stream + { + private readonly Stream inner = inner; + private readonly Action onClose = onClose; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + onClose(inner); + } + base.Dispose(disposing); + } + + public override bool CanRead => inner.CanRead; + public override bool CanSeek => inner.CanSeek; + public override bool CanWrite => inner.CanWrite; + public override long Length => inner.Length; + public override long Position { get => inner.Position; set => inner.Position = value; } + public override void Flush() => inner.Flush(); + public override int Read(byte[] buffer, int offset, int count) => inner.Read(buffer, offset, count); + public override long Seek(long offset, SeekOrigin origin) => inner.Seek(offset, origin); + public override void SetLength(long value) => inner.SetLength(value); + public override void Write(byte[] buffer, int offset, int count) => inner.Write(buffer, offset, count); + } + +} + +public class ZipBuilder(string root) : ZipFileSystem(root), ILimitedFileSystem, IDisposable +{ + // key: normalized path, value: bytes + private readonly Dictionary files = []; + + public Stream FileOpenWrite(string path) + { + path = Absolute(SlashFix(path)); + var ms = new MemoryStream(); + return new StreamWithCallback(ms, s => + { + s.Position = 0; + files[path] = ((MemoryStream)s).ToArray(); + }); + } + + public void FileWriteAllBytes(string path, byte[] bytes) + { + path = Absolute(SlashFix(path)); + files[path] = bytes; + } + + public byte[] GetAllBytes() + { + using var output = new MemoryStream(); + using (var zip = new ZipArchive(output, ZipArchiveMode.Create, leaveOpen: true)) + { + foreach (var kvp in files) + { + var entry = zip.CreateEntry(kvp.Key, CompressionLevel.Optimal); + + using var entryStream = entry.Open(); + entryStream.Write(kvp.Value, 0, kvp.Value.Length); + } + } + + return output.ToArray(); + } + + void IDisposable.Dispose() + { + //for now nothing to dispose + } + + //Bypassed operationss + bool ILimitedFileSystem.DirectoryExists(string path) => false; + DirectoryInfo ILimitedFileSystem.CreateDirectory(string path) => new(Absolute(SlashFix(path))); + + // Unsupported operations + DirectoryInfo[] ILimitedFileSystem.GetDirectories(string path) => throw new NotSupportedException(); + void ILimitedFileSystem.DeleteDirectory(string path, bool recursive) => throw new NotSupportedException(); + string[] ILimitedFileSystem.GetFiles(string path, string searchPattern) => []; + byte[] ILimitedFileSystem.FileReadAllBytes(string path) => throw new NotSupportedException(); + Stream ILimitedFileSystem.FileOpenRead(string path) => throw new NotSupportedException(); + void ILimitedFileSystem.FileDelete(string path) => throw new NotSupportedException(); +} + +public sealed class ZipLoader : ZipFileSystem, ILimitedFileSystem, IDisposable +{ + private readonly ZipArchive zip; + private readonly string[] entriesPath; //No trailing slash + + private static string SlashEnd(string path) => path.HasText() ? SlashFix(path).TrimEnd('/', '\\') + '/' : ""; + + private static Regex GetPatternRegex(string basePath, string searchPattern) + { + basePath = Regex.Escape(SlashFix(basePath).TrimEnd('/', '\\') + '/'); + searchPattern = Regex.Escape(searchPattern) + .Replace(@"\*", ".*") + .Replace(@"\?", "."); + + return new Regex($"^{basePath}{searchPattern}$", RegexOptions.IgnoreCase); + } + + public ZipLoader(byte[] bytes, string root) : base(root) + { + zip = new(new MemoryStream(bytes), ZipArchiveMode.Read); + + entriesPath = ( + from e in zip.Entries + let f = SlashFix(e.FullName).TrimEnd('/', '\\') + where f.StartsWith(SlashEnd(root), StringComparison.OrdinalIgnoreCase) + select f + ) + .ToArray(); + } + + public ZipLoader(string zipFile, string root) : this(File.ReadAllBytes(zipFile), root) { } + + Stream ILimitedFileSystem.FileOpenRead(string path) + { + var entry = zip.GetEntry(SlashFix(path)) + ?? throw new FileNotFoundException(path); + + return entry.Open(); + } + + byte[] ILimitedFileSystem.FileReadAllBytes(string path) + { + var entry = zip.Entries.FirstOrDefault(e => e.FullName.Equals(SlashFix(path), StringComparison.OrdinalIgnoreCase)) + ?? throw new FileNotFoundException($"File '{path}' not found in zip."); + + using var entryStream = entry.Open(); + using var ms = new MemoryStream(); + entryStream.CopyTo(ms); + return ms.ToArray(); + } + + bool ILimitedFileSystem.DirectoryExists(string path) + { + return entriesPath.Any(e => e.StartsWith(Absolute(SlashFix(path)), StringComparison.OrdinalIgnoreCase)); + } + + DirectoryInfo[] ILimitedFileSystem.GetDirectories(string path) + { + path = SlashEnd(path); + + var dirs = ( + from entry in entriesPath + where entry.StartsWith(path, StringComparison.OrdinalIgnoreCase) + let remainder = entry[path.Length..] + where remainder.Contains('/') + select remainder[..remainder.IndexOf('/')] + ) + .Distinct(StringComparer.OrdinalIgnoreCase); + + return dirs + .Select(f => new DirectoryInfo(f)) + .ToArray(); + } + + string[] ILimitedFileSystem.GetFiles(string path, string searchPattern) + { + var regex = GetPatternRegex(Absolute(path), searchPattern); + + return entriesPath + .Where(f => regex.IsMatch(f)) + .ToArray(); + } + + void IDisposable.Dispose() + { + zip.Dispose(); + } + + // Unsupported operations + DirectoryInfo ILimitedFileSystem.CreateDirectory(string path) => throw new NotSupportedException(); + void ILimitedFileSystem.DeleteDirectory(string path, bool recursive) => throw new NotSupportedException(); + void ILimitedFileSystem.FileDelete(string path) => throw new NotSupportedException(); + Stream ILimitedFileSystem.FileOpenWrite(string path) => throw new NotSupportedException(); + void ILimitedFileSystem.FileWriteAllBytes(string path, byte[] bytes) => throw new NotSupportedException(); +} + diff --git a/Extensions/Signum.Help/AppendixHelp.cs b/Extensions/Signum.Help/AppendixHelp.cs index 1bd05331cc..7521f44a2b 100644 --- a/Extensions/Signum.Help/AppendixHelp.cs +++ b/Extensions/Signum.Help/AppendixHelp.cs @@ -4,7 +4,7 @@ namespace Signum.Help; [EntityKind(EntityKind.Main, EntityData.Master)] -public class AppendixHelpEntity : Entity, IHelpImageTarget +public class AppendixHelpEntity : Entity, IHelpEntity { [StringLengthValidator(Min = 3, Max = 100)] public string UniqueName { get; set; } @@ -17,7 +17,7 @@ public class AppendixHelpEntity : Entity, IHelpImageTarget [StringLengthValidator(Min = 3, MultiLine = true)] public string? Description { get; set; } - bool IHelpImageTarget.ForeachHtmlField(Func processHtml) + bool IHelpEntity.ForeachHtmlField(Func processHtml) { bool changed = false; if(Description != null) diff --git a/Extensions/Signum.Help/HelpAttachment.cs b/Extensions/Signum.Help/HelpAttachment.cs index 0a430c6ea3..e19d5bb15e 100644 --- a/Extensions/Signum.Help/HelpAttachment.cs +++ b/Extensions/Signum.Help/HelpAttachment.cs @@ -7,7 +7,7 @@ namespace Signum.Help; public class HelpImageEntity : Entity { [ImplementedBy(typeof(AppendixHelpEntity), typeof(NamespaceHelpEntity), typeof(QueryHelpEntity), typeof(TypeHelpEntity))] - public Lite Target { get; set; } + public Lite Target { get; set; } public DateTime CreationDate { get; set; } = Clock.Now; @@ -16,8 +16,9 @@ public class HelpImageEntity : Entity } -public interface IHelpImageTarget : IEntity +public interface IHelpEntity : IEntity { + public CultureInfoEntity Culture { get; set; } bool ForeachHtmlField(Func processHtml); } diff --git a/Extensions/Signum.Help/HelpClient.tsx b/Extensions/Signum.Help/HelpClient.tsx index 1826e1698f..29722bcd9d 100644 --- a/Extensions/Signum.Help/HelpClient.tsx +++ b/Extensions/Signum.Help/HelpClient.tsx @@ -1,19 +1,21 @@ import * as React from 'react' import { RouteObject } from 'react-router' -import { ajaxGet, ajaxPost } from '@framework/Services'; +import { Type } from '@framework/Reflection' +import { ajaxGet, ajaxPost, ajaxPostRaw, saveFile } from '@framework/Services'; import * as AppContext from '@framework/AppContext' import { OperationSymbol } from '@framework/Signum.Operations' import { PropertyRoute, PseudoType, QueryKey, getQueryKey, getTypeName, getTypeInfo, getAllTypes, getQueryInfo, tryGetTypeInfo } from '@framework/Reflection' import { ImportComponent } from '@framework/ImportComponent' import "./Help.css" -import { NamespaceHelpEntity, TypeHelpEntity, AppendixHelpEntity, HelpPermissions } from './Signum.Help'; +import { QuickLinkClient, QuickLinkAction } from '@framework/QuickLinkClient' +import { NamespaceHelpEntity, TypeHelpEntity, AppendixHelpEntity, QueryHelpEntity, HelpPermissions, IHelpEntity, HelpMessage } from './Signum.Help'; import { QueryString } from '@framework/QueryString'; import { OmniboxClient } from '../Signum.Omnibox/OmniboxClient'; import HelpOmniboxProvider from './HelpOmniboxProvider'; import { CultureInfoEntity } from '@framework/Signum.Basics'; import { WidgetContext, onWidgets } from '@framework/Frames/Widgets'; import { HelpIcon, HelpWidget } from './HelpWidget'; -import { Entity, isEntity } from '@framework/Signum.Entities'; +import { Entity, isEntity, Lite } from '@framework/Signum.Entities'; import { tasks } from '@framework/Lines'; import { LineBaseController, LineBaseProps } from '@framework/Lines/LineBase'; import { ChangeLogClient } from '@framework/Basics/ChangeLogClient'; @@ -35,8 +37,25 @@ export namespace HelpClient { tasks.push(taskHelpIcon); + registerExportLink(TypeHelpEntity); + registerExportLink(NamespaceHelpEntity); + registerExportLink(AppendixHelpEntity); + registerExportLink(QueryHelpEntity); + } + export function registerExportLink(type: Type): void { + if (AppContext.isPermissionAuthorized(HelpPermissions.ExportHelp)) + QuickLinkClient.registerQuickLink(type, + new QuickLinkAction(HelpMessage.ExportAsZip.name, () => HelpMessage.ExportAsZip.niceToString(), ctx => API.exportHelpEntities(ctx.lites), { + allowsMultiple: true, + iconColor: "#FCAE25", + icon: "file-code" + })); + } + + + export function taskHelpIcon(lineBase: LineBaseController, state: LineBaseProps) : void { if (state.labelIcon === undefined && state.ctx.propertyRoute && @@ -124,6 +143,13 @@ export namespace HelpClient { export function saveAppendix(appendix: AppendixHelpEntity): Promise { return ajaxPost({ url: "/api/help/saveAppendix" }, appendix); } + + export function exportHelpEntities(entity: Lite[]): void { + ajaxPostRaw({ url: "/api/help/export" }, entity) + .then(resp => saveFile(resp)); + } + + } export interface HelpIndexTS { diff --git a/Extensions/Signum.Help/HelpController.cs b/Extensions/Signum.Help/HelpController.cs index e6c30739e6..9a26b209c2 100644 --- a/Extensions/Signum.Help/HelpController.cs +++ b/Extensions/Signum.Help/HelpController.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Mvc; -using System.ComponentModel.DataAnnotations; -using System.Globalization; using Signum.API.Filters; using Signum.Basics; +using Signum.UserAssets; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.IO; namespace Signum.Help; @@ -44,7 +46,7 @@ public NamespaceHelp Namespace(string @namespace) } [HttpPost("api/help/saveNamespace")] - public void SaveNamespace([Required][FromBody]NamespaceHelpEntity entity) + public void SaveNamespace([Required][FromBody] NamespaceHelpEntity entity) { HelpPermissions.ViewHelp.AssertAuthorized(); @@ -91,7 +93,7 @@ public TypeHelpEntity Type(string cleanName) } [HttpPost("api/help/saveType")] - public void SaveType([Required][FromBody]TypeHelpEntity entity) + public void SaveType([Required][FromBody] TypeHelpEntity entity) { HelpPermissions.ViewHelp.AssertAuthorized(); @@ -137,6 +139,20 @@ public void SaveType([Required][FromBody]TypeHelpEntity entity) } } + + [HttpPost("api/help/export")] + public FileStreamResult Export([Required, FromBody] Lite[] lites) + { + HelpPermissions.ExportHelp.AssertAuthorized(); + + var bytes = HelpXml.ExportToZipBytes(lites.RetrieveList().ToList()); + + var typeName = lites.Select(a => a.EntityType).Distinct().SingleEx().ToTypeEntity().CleanName; + var Ids = lites.ToString(a => a.Id.ToString(), "_"); + var fileName = $"{typeName}{Ids}.zip"; + + return MimeMapping.GetFileStreamResult(new MemoryStream(bytes), fileName); + } } public class HelpIndexTS diff --git a/Extensions/Signum.Help/HelpLogic.cs b/Extensions/Signum.Help/HelpLogic.cs index 96841bdbee..a0ede92b15 100644 --- a/Extensions/Signum.Help/HelpLogic.cs +++ b/Extensions/Signum.Help/HelpLogic.cs @@ -146,6 +146,7 @@ public static void Start(SchemaBuilder sb, IFileTypeAlgorithm helpImagesAlgorith invalidateWith: new InvalidateWith(typeof(QueryHelpEntity))); PermissionLogic.RegisterPermissions(HelpPermissions.ViewHelp); + PermissionLogic.RegisterPermissions(HelpPermissions.ExportHelp); if(sb.WebServerBuilder != null) { diff --git a/Extensions/Signum.Help/HelpMessage.cs b/Extensions/Signum.Help/HelpMessage.cs index ee01fa0ad8..f0d5ed3e12 100644 --- a/Extensions/Signum.Help/HelpMessage.cs +++ b/Extensions/Signum.Help/HelpMessage.cs @@ -84,6 +84,7 @@ public enum HelpMessage Close, ViewMore, JumpToViewMore, + ExportAsZip, } public enum HelpKindMessage diff --git a/Extensions/Signum.Help/HelpModuleOmniboxResult.cs b/Extensions/Signum.Help/HelpModuleOmniboxResult.cs index 9111af57e0..1b69db8d2f 100644 --- a/Extensions/Signum.Help/HelpModuleOmniboxResult.cs +++ b/Extensions/Signum.Help/HelpModuleOmniboxResult.cs @@ -98,5 +98,5 @@ public override string ToString() public static class HelpPermissions { public static PermissionSymbol ViewHelp; - public static PermissionSymbol DownloadHelp; + public static PermissionSymbol ExportHelp; } diff --git a/Extensions/Signum.Help/HelpXml.cs b/Extensions/Signum.Help/HelpXml.cs index 9540a96486..0e85431ae9 100644 --- a/Extensions/Signum.Help/HelpXml.cs +++ b/Extensions/Signum.Help/HelpXml.cs @@ -1,17 +1,14 @@ -using DocumentFormat.OpenXml.Bibliography; -using DocumentFormat.OpenXml.Vml.Office; -using DocumentFormat.OpenXml.Wordprocessing; using Signum.API; -using Signum.Basics; using Signum.Engine.Sync; -using Signum.Engine.Sync.Postgres; -using Signum.Entities; using Signum.Files; using System.Globalization; -using System.IO; using System.Text.RegularExpressions; using System.Xml.Linq; +// Note: Avoid `using System.IO` to prevent accidental use in HelpXml. +// Use FileSystemScope for all file system operations instead. +using FS = Signum.Files.FileSystemScope; + namespace Signum.Help; public static class HelpXml @@ -38,22 +35,23 @@ public static XDocument ToXDocument(AppendixHelpEntity entity) } - internal static void LoadDirectory(string directory, CultureInfoEntity ci, Dictionary, List> images, + internal static void LoadDirectory(string directory, CultureInfoEntity ci, Dictionary, List> images, Replacements rep, ref bool? deleteAll) { + SafeConsole.WriteLineColor(ConsoleColor.White, $" Appendix"); var current = Database.Query().Where(a => a.Culture.Is(ci)).ToList(); - var files = Directory.Exists(directory) ? Directory.GetFiles(directory, "*.help") : null; + var files = FS.Directory.Exists(directory) ? FS.Directory.GetFiles(directory, "*.help") : null; XElement ParseXML(string path) { - XDocument doc = XDocument.Load(path); + XDocument doc = LoadXml(path); XElement element = doc.Element(_Appendix)!; var uniqueName = element.Attribute(_Name)!.Value; - if (uniqueName != Path.GetFileNameWithoutExtension(path)) + if (uniqueName != FS.Path.GetFileNameWithoutExtension(path)) throw new InvalidOperationException($"UniqueName attribute ({uniqueName}) does not match with file name ({path})"); var culture = element.Attribute(_Culture)!.Value; @@ -67,9 +65,9 @@ XElement ParseXML(string path) bool? deleteTemp = deleteAll; Synchronizer.SynchronizeReplacing(rep, "Appendix", - newDictionary: files.EmptyIfNull().ToDictionaryEx(o => Path.GetFileNameWithoutExtension(o)), + newDictionary: files.EmptyIfNull().ToDictionaryEx(o => FS.Path.GetFileNameWithoutExtension(o)), oldDictionary: current.ToDictionaryEx(n => n.UniqueName), - createNew: (k, n) => + createNew: (_, n) => { XElement element = ParseXML(n); @@ -153,22 +151,23 @@ internal static string GetNamespaceName(XDocument document, string fileName) return result; } - internal static void LoadDirectory(string directory, CultureInfoEntity ci, Dictionary, List> images, + internal static void LoadDirectory(string directory, CultureInfoEntity ci, Dictionary, List> images, Replacements rep, ref bool? deleteAll) { + SafeConsole.WriteLineColor(ConsoleColor.White, $" Namespace"); var current = Database.Query().Where(a => a.Culture.Is(ci)).ToList(); - var files = Directory.Exists(directory) ? Directory.GetFiles(directory, "*.help") : null; + var files = FS.Directory.Exists(directory) ? FS.Directory.GetFiles(directory, "*.help") : null; XElement ParseXML(string path) { - XDocument doc = XDocument.Load(path); + XDocument doc = LoadXml(path); XElement element = doc.Element(_Namespace)!; var uniqueName = element.Attribute(_Name)!.Value; - if (uniqueName != Path.GetFileNameWithoutExtension(path)) + if (uniqueName != FS.Path.GetFileNameWithoutExtension(path)) throw new InvalidOperationException($"UniqueName attribute ({uniqueName}) does not match with file name ({path})"); var culture = element.Attribute(_Culture)!.Value; @@ -181,9 +180,9 @@ XElement ParseXML(string path) bool? deleteTemp = deleteAll; Synchronizer.SynchronizeReplacing(rep, "Namespace", - newDictionary: files.EmptyIfNull().ToDictionaryEx(o => Path.GetFileName(o)), + newDictionary: files.EmptyIfNull().ToDictionaryEx(o => FS.Path.GetFileNameWithoutExtension(o)), oldDictionary: current.ToDictionaryEx(n => n.Name), - createNew: (k, n) => + createNew: (_, n) => { XElement element = ParseXML(n); @@ -262,22 +261,22 @@ public static XDocument ToXDocument(QueryHelpEntity entity) ); } - internal static void LoadDirectory(string directory, CultureInfoEntity ci, Dictionary, List> images, + internal static void LoadDirectory(string directory, CultureInfoEntity ci, Dictionary, List> images, Replacements rep, ref bool? deleteAll) { SafeConsole.WriteLineColor(ConsoleColor.White, $" Query"); var current = Database.Query().Where(a => a.Culture.Is(ci)).ToList(); - var files = Directory.Exists(directory) ? Directory.GetFiles(directory, "*.help") : null; + var files = FS.Directory.Exists(directory) ? FS.Directory.GetFiles(directory, "*.help") : null; XElement ParseXML(string path) { - XDocument doc = XDocument.Load(path); + XDocument doc = LoadXml(path); XElement element = doc.Element(_Query)!; var queryKey = element.Attribute(_Key)!.Value; - if (queryKey != Path.GetFileNameWithoutExtension(path)) + if (queryKey != FS.Path.GetFileNameWithoutExtension(path)) throw new InvalidOperationException($"Key attribute ({queryKey}) does not match with file name ({path})"); var culture = element.Attribute(_Culture)!.Value; @@ -290,12 +289,12 @@ XElement ParseXML(string path) bool? deleteTemp = deleteAll; Synchronizer.SynchronizeReplacing(rep, "Queries", - newDictionary: files.EmptyIfNull().ToDictionaryEx(o => Path.GetFileName(o)), + newDictionary: files.EmptyIfNull().ToDictionaryEx(o => FS.Path.GetFileNameWithoutExtension(o)), oldDictionary: current.ToDictionaryEx(n => n.Query.Key), createNew: (k, n) => { XElement element = ParseXML(n); - var qn = QueryLogic.ToQueryName(k.Before(".help")); + var qn = QueryLogic.ToQueryName(k); var queryHelp = new QueryHelpEntity { Culture = ci, @@ -498,24 +497,25 @@ public static string GetEntityFullName(XDocument document, string fileName) } - internal static void LoadDirectory(string directory, CultureInfoEntity ci, Dictionary, List> images, + internal static void LoadDirectory(string directory, CultureInfoEntity ci, Dictionary, List> images, Replacements rep, ref bool? deleteAll) { + SafeConsole.WriteLineColor(ConsoleColor.White, $" Entity"); var typesByFullName = HelpLogic.AllTypes().ToDictionary(a => TypeLogic.GetCleanName(a)!); var current = Database.Query().Where(a => a.Culture.Is(ci)).ToList(); - var files = Directory.Exists(directory) ? Directory.GetFiles(directory, "*.help") : null; + var files = FS.Directory.Exists(directory) ? FS.Directory.GetFiles(directory, "*.help") : null; XElement ParseXML(string path) { - XDocument doc = XDocument.Load(path); + XDocument doc = LoadXml(path); XElement element = doc.Element(_Entity)!; var uniqueName = element.Attribute(_CleanName)!.Value; - if (uniqueName != Path.GetFileNameWithoutExtension(path)) + if (uniqueName != FS.Path.GetFileNameWithoutExtension(path)) throw new InvalidOperationException($"UniqueName attribute ({uniqueName}) does not match with file name ({path})"); var culture = element.Attribute(_Culture)!.Value; @@ -528,9 +528,9 @@ XElement ParseXML(string path) bool? deleteTemp = null; Synchronizer.SynchronizeReplacing(rep, "Types", - newDictionary: files.EmptyIfNull().ToDictionaryEx(o => Path.GetFileNameWithoutExtension(o)), + newDictionary: files.EmptyIfNull().ToDictionaryEx(o => FS.Path.GetFileNameWithoutExtension(o)), oldDictionary: current.ToDictionaryEx(n => n.Type.CleanName), - createNew: (k, path) => + createNew: (_, path) => { XElement element = ParseXML(path); var cleanName = element.Attribute(_CleanName)!.Value; @@ -584,19 +584,20 @@ XElement ParseXML(string path) private static void ImportImages(Entity entity, string filePath, List? images) { - var imageDir = Path.Combine(Path.GetDirectoryName(filePath)!, Path.GetFileNameWithoutExtension(filePath)); - var newImages = Directory.Exists(imageDir) ? Directory.GetFiles(imageDir) : null; + + var imageDir = FS.Path.Combine(FS.Path.GetDirectoryName(filePath)!, FS.Path.GetFileNameWithoutExtension(filePath)); + var newImages = FS.Directory.Exists(imageDir) ? FS.Directory.GetFiles(imageDir, "*") : null; Synchronizer.Synchronize( - newDictionary: newImages.EmptyIfNull().ToDictionaryEx(n => Path.GetFileName(n)), + newDictionary: newImages.EmptyIfNull().ToDictionaryEx(n => FS.Path.GetFileName(n)), oldDictionary: images.EmptyIfNull().ToDictionaryEx(o => o.Id + "." + o.File.FileName), createNew: (k, n) => { Administrator.SaveDisableIdentity(new HelpImageEntity { - Target = ((IHelpImageTarget)entity).ToLite(), - File = new FilePathEmbedded(HelpImageFileType.Image, Path.GetFileName(n).After("."), File.ReadAllBytes(n)) - }.SetId(Guid.Parse(Path.GetFileName(n).Before(".")))); + Target = ((IHelpEntity)entity).ToLite(), + File = new FilePathEmbedded(HelpImageFileType.Image, FS.Path.GetFileName(n).After("."), FS.File.ReadAllBytes(n)) + }.SetId(Guid.Parse(FS.Path.GetFileName(n).Before(".")))); SafeConsole.WriteColor(ConsoleColor.Green, '.'); }, removeOld: (k, o) => @@ -639,7 +640,7 @@ private static void ImportImages(Entity entity, string filePath, List GetAllCultures(string directoryName) { + HashSet cultures = new HashSet(); cultures.AddRange(Database.Query().Select(a => a.Culture).Distinct().ToList().Select(c => c.ToCultureInfo())); cultures.AddRange(Database.Query().Select(a => a.Culture).Distinct().ToList().Select(c => c.ToCultureInfo())); cultures.AddRange(Database.Query().Select(a => a.Culture).Distinct().ToList().Select(c => c.ToCultureInfo())); cultures.AddRange(Database.Query().Select(a => a.Culture).Distinct().ToList().Select(c => c.ToCultureInfo())); - if (Directory.Exists(directoryName)) - cultures.AddRange(new DirectoryInfo(directoryName).GetDirectories().Select(c => CultureInfo.GetCultureInfo(c.Name))); + if (FS.Directory.Exists(directoryName)) + cultures.AddRange(FS.Directory.GetDirectories(directoryName).Select(c => CultureInfo.GetCultureInfo(c.Name))); return cultures; } @@ -668,40 +670,55 @@ public static void ExportCulture(string directoryName, CultureInfo ci) { bool? replace = null; bool? delete = null; - + var cie = ci.ToCultureInfoEntity(); var group = Database.Query().GroupToDictionary(a => a.Target); - ExportFolder(ref replace, ref delete, Path.Combine(directoryName, ci.Name, AppendicesDirectory), + ExportFolder(ref replace, ref delete, FS.Path.Combine(directoryName, ci.Name, AppendicesDirectory), Database.Query().Where(ah => ah.Culture.Is(cie)).ToList(), ah => "{0}.help".FormatWith(RemoveInvalid(ah.UniqueName), ah.Culture.Name), ah => AppendixXml.ToXDocument(ah), ah => group.TryGetC(ah.ToLite())); - ExportFolder(ref replace, ref delete, Path.Combine(directoryName, ci.Name, NamespacesDirectory), + ExportFolder(ref replace, ref delete, FS.Path.Combine(directoryName, ci.Name, NamespacesDirectory), Database.Query().Where(nh => nh.Culture.Is(cie)).ToList(), nh => "{0}.help".FormatWith(RemoveInvalid(nh.Name), nh.Culture.Name), nh => NamespaceXml.ToXDocument(nh), ah => group.TryGetC(ah.ToLite())); - ExportFolder(ref replace, ref delete, Path.Combine(directoryName, ci.Name, TypesDirectory), + ExportFolder(ref replace, ref delete, FS.Path.Combine(directoryName, ci.Name, TypesDirectory), Database.Query().Where(th => th.Culture.Is(cie)).ToList(), th => "{0}.help".FormatWith(RemoveInvalid(th.Type.CleanName), th.Culture.Name), th => EntityXml.ToXDocument(th), ah => group.TryGetC(ah.ToLite())); - ExportFolder(ref replace, ref delete, Path.Combine(directoryName, ci.Name, QueriesDirectory), + ExportFolder(ref replace, ref delete, FS.Path.Combine(directoryName, ci.Name, QueriesDirectory), Database.Query().Where(qh => qh.Culture.Is(cie)).ToList(), qh => "{0}.help".FormatWith(RemoveInvalid(qh.Query.Key), qh.Culture.Name), qh => QueryXml.ToXDocument(qh), ah => group.TryGetC(ah.ToLite())); } + private static void SaveXml(this XDocument doc, string fileName) + { + using var stream = FS.File.OpenWrite(fileName); + doc.Save(stream); + } + + private static XDocument LoadXml(string fileName) + { + using var stream = FS.File.OpenRead(fileName); + XDocument doc = XDocument.Load(stream); + + return doc; + } + public static void ExportFolder(ref bool? replace, ref bool? delete, string folder, List should, Func fileName, Func toXML, Func?> getImages) { - if (should.Any() && !Directory.Exists(folder)) - Directory.CreateDirectory(folder); + + if (should.Any() && !FS.Directory.Exists(folder)) + FS.Directory.CreateDirectory(folder); var deleteLocal = delete; var replaceLocal = replace; @@ -709,22 +726,22 @@ public static void ExportFolder(ref bool? replace, ref bool? delete, string f SafeConsole.WriteLineColor(ConsoleColor.Gray, "Exporting to " + folder); Synchronizer.Synchronize( newDictionary: should.ToDictionary(fileName), - oldDictionary: !Directory.Exists(folder) ? new() : Directory.GetFiles(folder).ToDictionary(a => Path.GetFileName(a)), + oldDictionary: !FS.Directory.Exists(folder) ? new() : FS.Directory.GetFiles(folder).ToDictionary(a => FS.Path.GetFileName(a)), createNew: (fileName, entity) => { - toXML(entity).Save(Path.Combine(folder, fileName)); + toXML(entity).SaveXml(FS.Path.Combine(folder, fileName)); SafeConsole.WriteColor(ConsoleColor.Green, " Created " + fileName); var images = getImages(entity); if (images != null) { - var imgDirectory = Path.Combine(folder, Path.GetFileNameWithoutExtension(fileName)); - Directory.CreateDirectory(imgDirectory); + var imgDirectory = FS.Path.Combine(folder, FS.Path.GetFileNameWithoutExtension(fileName)); + FS.Directory.CreateDirectory(imgDirectory); foreach (var img in images) { - var bla = Path.Combine(imgDirectory, img.Id + "." + img.File.FileName); - File.WriteAllBytes(bla, img.File.GetByteArray()); + var bla = FS.Path.Combine(imgDirectory, img.Id + "." + img.File.FileName); + FS.File.WriteAllBytes(bla, img.File.GetByteArray()); } } @@ -734,26 +751,27 @@ public static void ExportFolder(ref bool? replace, ref bool? delete, string f { if (SafeConsole.Ask(ref deleteLocal, "Delete {0}?".FormatWith(fileName))) { - File.Delete(fullName); + FS.File.Delete(fullName); SafeConsole.WriteLineColor(ConsoleColor.Red, " Deleted " + fileName); - var imgDirectory = Path.Combine(folder, Path.GetFileNameWithoutExtension(fileName)); - if (Directory.Exists(imgDirectory)) - Directory.Delete(imgDirectory, true); + var imgDirectory = FS.Path.Combine(folder, FS.Path.GetFileNameWithoutExtension(fileName)); + if (FS.Directory.Exists(imgDirectory)) + FS.Directory.Delete(imgDirectory, true); } }, merge: (fileName, entity, fullName) => { var xml = toXML(entity); - var newBytes = new MemoryStream().Do(ms => xml.Save(ms)).ToArray(); - var oldBytes = File.ReadAllBytes(fullName); + var newBytes = new System.IO.MemoryStream().Do(ms => xml.Save(ms)).ToArray(); + var oldBytes = FS.File.ReadAllBytes(fullName); if (!MemoryExtensions.SequenceEqual(newBytes, oldBytes)) { if (SafeConsole.Ask(ref replaceLocal, " Override {0}?".FormatWith(fileName))) { - xml.Save(Path.Combine(folder, fileName)); + + xml.SaveXml(FS.Path.Combine(folder, fileName)); SafeConsole.WriteColor(ConsoleColor.Yellow, " Overriden " + fileName); } } @@ -762,11 +780,11 @@ public static void ExportFolder(ref bool? replace, ref bool? delete, string f SafeConsole.WriteColor(ConsoleColor.DarkGray, " Identical " + fileName); } - var imgDirectory = Path.Combine(folder, Path.GetFileNameWithoutExtension(fileName)); + var imgDirectory = FS.Path.Combine(folder, FS.Path.GetFileNameWithoutExtension(fileName)); var images = getImages(entity); if (images != null) { - var currImages = Directory.GetFiles(imgDirectory).ToDictionary(a => Path.GetFileName(a)); + var currImages = FS.Directory.GetFiles(imgDirectory).ToDictionary(a => FS.Path.GetFileName(a)); var shouldImages = images.ToDictionaryEx(a => a.Id + "." + a.File.FileName); @@ -776,12 +794,12 @@ public static void ExportFolder(ref bool? replace, ref bool? delete, string f oldDictionary: currImages, createNew: (k, n) => { - File.WriteAllBytes(Path.Combine(imgDirectory, k), n.File.GetByteArray()); + FS.File.WriteAllBytes(FS.Path.Combine(imgDirectory, k), n.File.GetByteArray()); SafeConsole.WriteColor(ConsoleColor.DarkGreen, '.'); }, removeOld: (k, o) => { - File.Delete(Path.Combine(imgDirectory, k)); + FS.File.Delete(FS.Path.Combine(imgDirectory, k)); SafeConsole.WriteColor(ConsoleColor.DarkRed, '.'); }, merge: (k, n, o) => @@ -790,8 +808,8 @@ public static void ExportFolder(ref bool? replace, ref bool? delete, string f } else { - if (Directory.Exists(imgDirectory)) - Directory.Delete(imgDirectory, true); + if (FS.Directory.Exists(imgDirectory)) + FS.Directory.Delete(imgDirectory, true); } Console.WriteLine(); @@ -803,6 +821,7 @@ public static void ExportFolder(ref bool? replace, ref bool? delete, string f public static void ImportAll(string directoryName) { + var images = Database.Query().GroupToDictionary(a => a.Target); Replacements rep = new Replacements(); @@ -810,19 +829,114 @@ public static void ImportAll(string directoryName) foreach (var ci in GetAllCultures(directoryName)) { var ciEntity = ci.ToCultureInfoEntity(); - var dirCulture = Path.Combine(directoryName, ci.Name); + var dirCulture = FS.Path.Combine(directoryName, ci.Name); SafeConsole.WriteLineColor(ConsoleColor.White, $"{ciEntity.Name} ({ciEntity.EnglishName})"); - AppendixXml.LoadDirectory(Path.Combine(dirCulture, AppendicesDirectory), ciEntity, images, rep, ref deleteAll); - NamespaceXml.LoadDirectory(Path.Combine(dirCulture, NamespacesDirectory), ciEntity, images, rep, ref deleteAll); - EntityXml.LoadDirectory(Path.Combine(dirCulture, TypesDirectory), ciEntity, images, rep, ref deleteAll); - QueryXml.LoadDirectory(Path.Combine(dirCulture, QueriesDirectory), ciEntity, images, rep, ref deleteAll); + AppendixXml.LoadDirectory(FS.Path.Combine(dirCulture, AppendicesDirectory), ciEntity, images, rep, ref deleteAll); + NamespaceXml.LoadDirectory(FS.Path.Combine(dirCulture, NamespacesDirectory), ciEntity, images, rep, ref deleteAll); + EntityXml.LoadDirectory(FS.Path.Combine(dirCulture, TypesDirectory), ciEntity, images, rep, ref deleteAll); + QueryXml.LoadDirectory(FS.Path.Combine(dirCulture, QueriesDirectory), ciEntity, images, rep, ref deleteAll); Console.WriteLine(); } } + public static void Export(List entities, string directoryName) + { + if (entities == null || entities.Count == 0) + throw new InvalidOperationException("No entities to export"); + + var groupedByCulture = entities + .GroupBy(e => e.Culture) + .ToDictionary(g => g.Key, g => g.ToList()); + + bool? replace = null; + bool? delete = null; + + var imageGroups = Database.Query() + .Where(hi => entities.Any(t => hi.Target.Is(t))) + .GroupToDictionary(a => a.Target); + + foreach (var (cultureEntity, list) in groupedByCulture) + { + var ci = cultureEntity.ToCultureInfo(); + var cultureDir = FS.Path.Combine(directoryName, ci.Name); + + var typeHelps = list.OfType().ToList(); + if (typeHelps.Any()) + { + ExportFolder(ref replace, ref delete, FS.Path.Combine(cultureDir, TypesDirectory), + typeHelps, + th => $"{RemoveInvalid(th.Type.CleanName)}.help", + th => EntityXml.ToXDocument(th), + th => imageGroups.TryGetC(th.ToLite())); + } + + var namespaceHelps = list.OfType().ToList(); + if (namespaceHelps.Any()) + { + ExportFolder(ref replace, ref delete, FS.Path.Combine(cultureDir, NamespacesDirectory), + namespaceHelps, + nh => $"{RemoveInvalid(nh.Name)}.help", + nh => NamespaceXml.ToXDocument(nh), + nh => imageGroups.TryGetC(nh.ToLite())); + } + + var appendixHelps = list.OfType().ToList(); + if (appendixHelps.Any()) + { + ExportFolder(ref replace, ref delete, FS.Path.Combine(cultureDir, AppendicesDirectory), + appendixHelps, + ah => $"{RemoveInvalid(ah.UniqueName)}.help", + ah => AppendixXml.ToXDocument(ah), + ah => imageGroups.TryGetC(ah.ToLite())); + } + + var queryHelps = list.OfType().ToList(); + if (queryHelps.Any()) + { + ExportFolder(ref replace, ref delete, FS.Path.Combine(cultureDir, QueriesDirectory), + queryHelps, + qh => $"{RemoveInvalid(qh.Query.Key)}.help", + qh => QueryXml.ToXDocument(qh), + qh => imageGroups.TryGetC(qh.ToLite())); + } + } + } + + public static byte[] ExportToZipBytes(List entities) + { + var zip = new ZipBuilder("Help"); + using (new FS(zip)) + Export(entities, ""); + var bytes = zip.GetAllBytes(); + return bytes; + } + + public static byte[] ExportAllToZipBytes() + { + var zip = new ZipBuilder(""); + using (new FS(zip)) + ExportAll("Help"); + var bytes = zip.GetAllBytes(); + return bytes; + } + + public static void ImportAllFromZip() + { + var zip = new ZipLoader(@"..\..\..\Help.zip", ""); + using (new FS(zip)) + ImportAll("Help"); + } + + public static void ExportAllToZipFile(string filePath) + { + var bytes = ExportAllToZipBytes(); + FS.File.WriteAllBytes(filePath, bytes);//uses Real FileSystem + } + + public static void ImportExportHelp() { ImportExportHelp(@"..\..\..\Help"); @@ -831,13 +945,15 @@ public static void ImportExportHelp() public static void ImportExportHelp(string directoryName) { retry: - Console.WriteLine("You want to export (e) or import (i) Help? (nothing to exit)"); + Console.WriteLine("You want to export (e), import (i), export ZIP (ez), or import ZIP (iz)? (nothing to exit)"); switch (Console.ReadLine()!.ToLower()) { case "": return; case "e": ExportAll(directoryName); break; + case "ez": ExportAllToZipFile(@"..\..\..\Help.zip"); break; case "i": ImportAll(directoryName); break; + case "iz": ImportAllFromZip(); break; default: goto retry; } diff --git a/Extensions/Signum.Help/InlineImagesLogic.cs b/Extensions/Signum.Help/InlineImagesLogic.cs index 24f9e00fca..765759d2e5 100644 --- a/Extensions/Signum.Help/InlineImagesLogic.cs +++ b/Extensions/Signum.Help/InlineImagesLogic.cs @@ -8,12 +8,12 @@ public static class InlineImagesLogic { [AutoExpressionField] - public static IQueryable Images(this IHelpImageTarget e) => + public static IQueryable Images(this IHelpEntity e) => As.Expression(() => Database.Query().Where(a => a.Target.Is(e))); public static Regex ImgRegex = new Regex(@"[\w\-]+)\s*=\s*""(?[^""]+)"")+\s*/?>"); - public static bool SynchronizeInlineImages(IHelpImageTarget entity) + public static bool SynchronizeInlineImages(IHelpEntity entity) { using (OperationLogic.AllowSave()) { diff --git a/Extensions/Signum.Help/NamespaceHelp.cs b/Extensions/Signum.Help/NamespaceHelp.cs index 194fca8b0c..7aba9d0328 100644 --- a/Extensions/Signum.Help/NamespaceHelp.cs +++ b/Extensions/Signum.Help/NamespaceHelp.cs @@ -3,7 +3,7 @@ namespace Signum.Help; [EntityKind(EntityKind.Main, EntityData.Master)] -public class NamespaceHelpEntity : Entity, IHelpImageTarget +public class NamespaceHelpEntity : Entity, IHelpEntity { [StringLengthValidator(Max = 300)] public string Name { get; set; } @@ -19,7 +19,7 @@ public class NamespaceHelpEntity : Entity, IHelpImageTarget [AutoExpressionField] public override string ToString() => As.Expression(() => Name); - bool IHelpImageTarget.ForeachHtmlField(Func processHtml) + bool IHelpEntity.ForeachHtmlField(Func processHtml) { bool changed = false; if (Description != null) diff --git a/Extensions/Signum.Help/QueryHelp.cs b/Extensions/Signum.Help/QueryHelp.cs index 49944cbc64..1ab5dcc4ff 100644 --- a/Extensions/Signum.Help/QueryHelp.cs +++ b/Extensions/Signum.Help/QueryHelp.cs @@ -3,7 +3,7 @@ namespace Signum.Help; [EntityKind(EntityKind.SharedPart, EntityData.Master)] -public class QueryHelpEntity : Entity, IHelpImageTarget +public class QueryHelpEntity : Entity, IHelpEntity { public QueryEntity Query { get; set; } @@ -35,7 +35,7 @@ public bool IsEmpty [AutoExpressionField] public override string ToString() => As.Expression(() => (IsNew ? "" : Query.ToString())); - bool IHelpImageTarget.ForeachHtmlField(Func processHtml) + bool IHelpEntity.ForeachHtmlField(Func processHtml) { bool changed = false; if (Description != null) diff --git a/Extensions/Signum.Help/Signum.Help.ts b/Extensions/Signum.Help/Signum.Help.ts index 03d90c0366..ab43ed641f 100644 --- a/Extensions/Signum.Help/Signum.Help.ts +++ b/Extensions/Signum.Help/Signum.Help.ts @@ -10,7 +10,7 @@ import * as Files from '../Signum.Files/Signum.Files' export const AppendixHelpEntity: Type = new Type("AppendixHelp"); -export interface AppendixHelpEntity extends Entities.Entity, IHelpImageTarget { +export interface AppendixHelpEntity extends Entities.Entity, IHelpEntity { Type: "AppendixHelp"; uniqueName: string; culture: Basics.CultureInfoEntity; @@ -26,7 +26,7 @@ export namespace AppendixHelpOperation { export const HelpImageEntity: Type = new Type("HelpImage"); export interface HelpImageEntity extends Entities.Entity { Type: "HelpImage"; - target: Entities.Lite; + target: Entities.Lite; creationDate: string /*DateTime*/; file: Files.FilePathEmbedded; } @@ -95,11 +95,12 @@ export namespace HelpMessage { export const Close: MessageKey = new MessageKey("HelpMessage", "Close"); export const ViewMore: MessageKey = new MessageKey("HelpMessage", "ViewMore"); export const JumpToViewMore: MessageKey = new MessageKey("HelpMessage", "JumpToViewMore"); + export const ExportAsZip: MessageKey = new MessageKey("HelpMessage", "ExportAsZip"); } export namespace HelpPermissions { export const ViewHelp : Basics.PermissionSymbol = registerSymbol("Permission", "HelpPermissions.ViewHelp"); - export const DownloadHelp : Basics.PermissionSymbol = registerSymbol("Permission", "HelpPermissions.DownloadHelp"); + export const ExportHelp : Basics.PermissionSymbol = registerSymbol("Permission", "HelpPermissions.ExportHelp"); } export namespace HelpSearchMessage { @@ -138,11 +139,12 @@ export namespace HelpSyntaxMessage { export const TranslateFrom: MessageKey = new MessageKey("HelpSyntaxMessage", "TranslateFrom"); } -export interface IHelpImageTarget extends Entities.Entity { +export interface IHelpEntity extends Entities.Entity { + culture: Basics.CultureInfoEntity; } export const NamespaceHelpEntity: Type = new Type("NamespaceHelp"); -export interface NamespaceHelpEntity extends Entities.Entity, IHelpImageTarget { +export interface NamespaceHelpEntity extends Entities.Entity, IHelpEntity { Type: "NamespaceHelp"; name: string; culture: Basics.CultureInfoEntity; @@ -181,7 +183,7 @@ export interface QueryColumnHelpEmbedded extends Entities.EmbeddedEntity { } export const QueryHelpEntity: Type = new Type("QueryHelp"); -export interface QueryHelpEntity extends Entities.Entity, IHelpImageTarget { +export interface QueryHelpEntity extends Entities.Entity, IHelpEntity { Type: "QueryHelp"; query: Basics.QueryEntity; culture: Basics.CultureInfoEntity; @@ -197,7 +199,7 @@ export namespace QueryHelpOperation { } export const TypeHelpEntity: Type = new Type("TypeHelp"); -export interface TypeHelpEntity extends Entities.Entity, IHelpImageTarget { +export interface TypeHelpEntity extends Entities.Entity, IHelpEntity { Type: "TypeHelp"; type: Basics.TypeEntity; culture: Basics.CultureInfoEntity; diff --git a/Extensions/Signum.Help/TypeHelp.cs b/Extensions/Signum.Help/TypeHelp.cs index 916e4d090a..960b507043 100644 --- a/Extensions/Signum.Help/TypeHelp.cs +++ b/Extensions/Signum.Help/TypeHelp.cs @@ -3,7 +3,7 @@ namespace Signum.Help; [EntityKind(EntityKind.Main, EntityData.Master)] -public class TypeHelpEntity : Entity, IHelpImageTarget +public class TypeHelpEntity : Entity, IHelpEntity { public TypeEntity Type { get; set; } @@ -40,7 +40,7 @@ public bool IsEmpty return base.PropertyValidation(pi); } - bool IHelpImageTarget.ForeachHtmlField(Func processHtml) + bool IHelpEntity.ForeachHtmlField(Func processHtml) { bool changed = false; if (Description != null)