diff --git a/src/DocumentationBrowserViewExtension/DocumentationBrowserViewExtension.cs b/src/DocumentationBrowserViewExtension/DocumentationBrowserViewExtension.cs index b682f71d503..ec7af8b9b76 100644 --- a/src/DocumentationBrowserViewExtension/DocumentationBrowserViewExtension.cs +++ b/src/DocumentationBrowserViewExtension/DocumentationBrowserViewExtension.cs @@ -4,7 +4,10 @@ using System.IO; using System.Linq; using System.Security.Permissions; +using System.Text; +using System.Text.RegularExpressions; using System.Threading; +using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; using Dynamo.Configuration; @@ -15,12 +18,14 @@ using Dynamo.Logging; using Dynamo.Models; using Dynamo.PackageManager; +using Dynamo.Search.SearchElements; using Dynamo.Selection; using Dynamo.ViewModels; using Dynamo.Wpf.Extensions; using Dynamo.Wpf.Interfaces; using DynamoProperties = Dynamo.Properties; using MenuItem = System.Windows.Controls.MenuItem; +using Microsoft.Win32; namespace Dynamo.DocumentationBrowser { @@ -153,6 +158,9 @@ public override void Loaded(ViewLoadedParams viewLoadedParams) // subscribe to the documentation open request event from Dynamo this.viewLoadedParamsReference.RequestOpenDocumentationLink += HandleRequestOpenDocumentationLink; + // subscribe to node help audit request (e.g. from Debug menu) + this.viewLoadedParamsReference.NodeHelpAuditRequested += OnNodeHelpAuditRequested; + // subscribe to property changes of DynamoViewModel so we can show/hide the browser on StartPage display (viewLoadedParams.DynamoWindow.DataContext as DynamoViewModel).PropertyChanged += HandleStartPageVisibilityChange; @@ -408,6 +416,408 @@ private void OnPackageLoaded(Package pkg) PackageDocumentationManager.Instance.AddPackageDocumentation(pkg.NodeDocumentaionDirectory, pkg.Name); } + private void OnNodeHelpAuditRequested() + { + RunNodeHelpAudit(); + } + + internal void RunNodeHelpAudit() + { + if (DynamoViewModel == null || ViewModel == null) + { + OnMessageLogged(LogMessage.Warning(Resources.NodeHelpAuditNotReady, WarningLevel.Mild)); + return; + } + + var docManager = PackageDocumentationManager.Instance; + if (docManager == null) + { + OnMessageLogged(LogMessage.Warning(Resources.NodeHelpAuditManagerMissing, WarningLevel.Mild)); + return; + } + + var saveDialog = new SaveFileDialog + { + Filter = Resources.NodeHelpAuditSaveDialogFilter, + DefaultExt = ".csv", + AddExtension = true, + FileName = $"NodeHelpAudit_{DateTime.Now:yyyyMMdd_HHmmss}.csv", + Title = Resources.NodeHelpAuditSaveDialogTitle + }; + + var owner = viewLoadedParamsReference?.DynamoWindow; + var dialogResult = owner == null ? saveDialog.ShowDialog() : saveDialog.ShowDialog(owner); + if (dialogResult != true) + { + return; + } + + var targetPath = saveDialog.FileName; + try + { + var entries = DynamoViewModel.Model?.SearchModel?.Entries?.Where(entry => entry.IsVisibleInSearch).ToList(); + if (entries == null || entries.Count == 0) + { + return; + } + + var packages = pmExtension?.PackageLoader?.LocalPackages?.ToList() ?? new List(); + var packageRoots = BuildPackageRootIndex(packages); + var packageAssemblies = BuildPackageAssemblyLookup(packages); + + // Collect all per-entry data on the UI thread (CreateNode, GetMinimumQualifiedName, etc. are not thread-safe). + var auditRows = new List(); + foreach (var entry in entries) + { + try + { + var node = entry.CreateNode(); + var minimumQualifiedName = DynamoViewModel.GetMinimumQualifiedName(node); + var packageName = ResolvePackageName(entry, packageRoots, packageAssemblies); + + var mdPath = docManager.GetAnnotationDoc(minimumQualifiedName, packageName) ?? string.Empty; + var isBuiltInByPath = !string.IsNullOrEmpty(packageName) && ViewModel.IsBuiltInDocPath(mdPath); + var isOwnedByPackage = !string.IsNullOrEmpty(packageName) && !isBuiltInByPath; + + var sampleGraphPath = string.IsNullOrWhiteSpace(mdPath) + ? string.Empty + : ViewModel.DynamoGraphFromMDFilePath(mdPath, isOwnedByPackage); + + var category = entry.FullCategoryName ?? string.Empty; + var library = GetLibraryName(category); + + auditRows.Add(new NodeHelpAuditRowDto( + library, + category, + entry.Name ?? string.Empty, + entry.FullName ?? string.Empty, + mdPath, + sampleGraphPath)); + } + catch (Exception) + { + } + } + + // Only file I/O and string building on the background thread. + _ = Task.Run(() => + { + try + { + var csv = new StringBuilder(); + var csvHeader = Resources.NodeHelpAuditCsvHeader; + if (string.IsNullOrWhiteSpace(csvHeader)) + { + csvHeader = "Library,Category,Name,FullName,MissingMd,MissingDyn,MissingImage,MarkdownPath,SampleGraphPath,ImagePaths"; + } + csv.AppendLine(csvHeader); + + foreach (var row in auditRows) + { + try + { + var missingMd = string.IsNullOrWhiteSpace(row.MdPath) || !File.Exists(row.MdPath); + var missingDyn = string.IsNullOrWhiteSpace(row.SampleGraphPath) || !File.Exists(row.SampleGraphPath); + + var imagePaths = GetImagePathsFromMarkdownFile(row.MdPath); + var missingImage = imagePaths.Count > 0 && imagePaths.Any(path => !File.Exists(path)); + var imagePathsValue = imagePaths.Count == 0 ? string.Empty : string.Join(";", imagePaths); + + csv.AppendLine(string.Join(",", + EscapeCsv(row.Library), + EscapeCsv(row.Category), + EscapeCsv(row.Name), + EscapeCsv(row.FullName), + missingMd, + missingDyn, + missingImage, + EscapeCsv(row.MdPath), + EscapeCsv(row.SampleGraphPath), + EscapeCsv(imagePathsValue))); + } + catch (Exception) + { + } + } + + File.WriteAllText(targetPath, csv.ToString(), new UTF8Encoding(false)); + } + catch (Exception) + { + } + }); + } + catch (Exception) + { + } + } + + /// + /// DTO for one node help audit row. Filled on the UI thread; file I/O and CSV fields derived on the background thread. + /// + private sealed class NodeHelpAuditRowDto + { + internal string Library { get; } + internal string Category { get; } + internal string Name { get; } + internal string FullName { get; } + internal string MdPath { get; } + internal string SampleGraphPath { get; } + + internal NodeHelpAuditRowDto(string library, string category, string name, string fullName, string mdPath, string sampleGraphPath) + { + Library = library ?? string.Empty; + Category = category ?? string.Empty; + Name = name ?? string.Empty; + FullName = fullName ?? string.Empty; + MdPath = mdPath ?? string.Empty; + SampleGraphPath = sampleGraphPath ?? string.Empty; + } + } + + private static string EscapeCsv(string value) + { + if (string.IsNullOrEmpty(value)) + { + return string.Empty; + } + + if (value.Contains("\"") || value.Contains(",") || value.Contains("\n") || value.Contains("\r")) + { + return "\"" + value.Replace("\"", "\"\"") + "\""; + } + + return value; + } + + private static List GetImagePathsFromMarkdownFile(string markdownPath) + { + if (string.IsNullOrWhiteSpace(markdownPath) || !File.Exists(markdownPath)) + { + return new List(); + } + + string markdownContent; + try + { + markdownContent = File.ReadAllText(markdownPath); + } + catch + { + return new List(); + } + + if (string.IsNullOrWhiteSpace(markdownContent)) + { + return new List(); + } + + var baseDirectory = Path.GetDirectoryName(markdownPath); + if (string.IsNullOrWhiteSpace(baseDirectory)) + { + return new List(); + } + + var results = new List(); + + foreach (Match match in Regex.Matches(markdownContent, @"!\[[^\]]*\]\((?[^)\s]+)[^)]*\)", RegexOptions.IgnoreCase)) + { + AddImagePath(match.Groups["path"].Value, baseDirectory, results); + } + + foreach (Match match in Regex.Matches(markdownContent, "]+src=[\"'](?[^\"']+)[\"']", RegexOptions.IgnoreCase)) + { + AddImagePath(match.Groups["path"].Value, baseDirectory, results); + } + + return results.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + } + + private static void AddImagePath(string rawPath, string baseDirectory, List results) + { + if (string.IsNullOrWhiteSpace(rawPath)) + { + return; + } + + var trimmed = rawPath.Trim().Trim('"', '\''); + if (trimmed.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + if (Uri.TryCreate(trimmed, UriKind.Absolute, out var uri)) + { + if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) + { + return; + } + + if (uri.Scheme == Uri.UriSchemeFile) + { + results.Add(uri.LocalPath); + return; + } + } + + var unescaped = Uri.UnescapeDataString(trimmed); + var combined = Path.IsPathRooted(unescaped) + ? unescaped + : Path.Combine(baseDirectory, unescaped); + + results.Add(Path.GetFullPath(combined)); + } + + private static string GetLibraryName(string category) + { + if (string.IsNullOrWhiteSpace(category)) + { + return string.Empty; + } + + var separatorIndex = category.IndexOf('.'); + return separatorIndex > 0 ? category.Substring(0, separatorIndex) : category; + } + + private static List<(string Root, string Name)> BuildPackageRootIndex(IEnumerable packages) + { + var roots = new List<(string Root, string Name)>(); + foreach (var package in packages) + { + if (string.IsNullOrWhiteSpace(package?.RootDirectory)) + { + continue; + } + + roots.Add((NormalizeDirectory(package.RootDirectory), package.Name)); + } + + return roots; + } + + private static PackageAssemblyLookup BuildPackageAssemblyLookup(IEnumerable packages) + { + var byPath = new Dictionary(StringComparer.OrdinalIgnoreCase); + var byFileName = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var package in packages) + { + if (package?.LoadedAssemblies == null) + { + continue; + } + + foreach (var assembly in package.LoadedAssemblies) + { + var localPath = assembly?.LocalFilePath; + if (string.IsNullOrWhiteSpace(localPath)) + { + continue; + } + + var fullPath = Path.GetFullPath(localPath); + if (!byPath.ContainsKey(fullPath)) + { + byPath.Add(fullPath, package.Name); + } + + var fileName = Path.GetFileName(fullPath); + if (!string.IsNullOrWhiteSpace(fileName) && !byFileName.ContainsKey(fileName)) + { + byFileName.Add(fileName, package.Name); + } + } + } + + return new PackageAssemblyLookup(byPath, byFileName); + } + + private static string ResolvePackageName(NodeSearchElement entry, List<(string Root, string Name)> packageRoots, PackageAssemblyLookup packageAssemblies) + { + if (entry == null) + { + return string.Empty; + } + + if (!entry.ElementType.HasFlag(ElementTypes.Packaged)) + { + return string.Empty; + } + + var path = GetEntryPath(entry); + if (!string.IsNullOrWhiteSpace(path) && packageRoots != null && packageRoots.Count > 0) + { + var fullPath = Path.GetFullPath(path); + foreach (var root in packageRoots) + { + if (fullPath.StartsWith(root.Root, StringComparison.OrdinalIgnoreCase)) + { + return root.Name ?? string.Empty; + } + } + } + + if (packageAssemblies != null) + { + var assemblyPath = entry.Assembly ?? string.Empty; + if (Path.IsPathRooted(assemblyPath)) + { + var fullAssemblyPath = Path.GetFullPath(assemblyPath); + if (packageAssemblies.ByPath.TryGetValue(fullAssemblyPath, out var packageName)) + { + return packageName ?? string.Empty; + } + } + + var assemblyFileName = Path.GetFileName(assemblyPath); + if (!string.IsNullOrWhiteSpace(assemblyFileName) && + packageAssemblies.ByFileName.TryGetValue(assemblyFileName, out var packageByFileName)) + { + return packageByFileName ?? string.Empty; + } + } + + return string.Empty; + } + + private static string GetEntryPath(NodeSearchElement entry) + { + if (entry is CustomNodeSearchElement customNode && !string.IsNullOrWhiteSpace(customNode.Path)) + { + return customNode.Path; + } + + if (!string.IsNullOrWhiteSpace(entry.Assembly) && Path.IsPathRooted(entry.Assembly)) + { + return entry.Assembly; + } + + return string.Empty; + } + + private static string NormalizeDirectory(string path) + { + var fullPath = Path.GetFullPath(path); + if (!fullPath.EndsWith(Path.DirectorySeparatorChar.ToString())) + { + fullPath += Path.DirectorySeparatorChar; + } + + return fullPath; + } + + private sealed class PackageAssemblyLookup + { + internal PackageAssemblyLookup(Dictionary byPath, Dictionary byFileName) + { + ByPath = byPath ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + ByFileName = byFileName ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + internal Dictionary ByPath { get; } + internal Dictionary ByFileName { get; } + } + private void MenuItemUnCheckedHandler(object sender, RoutedEventArgs e) { viewLoadedParamsReference.CloseExtensioninInSideBar(this); @@ -424,6 +834,7 @@ protected virtual void Dispose(bool disposing) if (this.viewLoadedParamsReference != null) { this.viewLoadedParamsReference.RequestOpenDocumentationLink -= HandleRequestOpenDocumentationLink; + this.viewLoadedParamsReference.NodeHelpAuditRequested -= OnNodeHelpAuditRequested; } if (this.ViewModel != null) diff --git a/src/DocumentationBrowserViewExtension/DocumentationBrowserViewModel.cs b/src/DocumentationBrowserViewExtension/DocumentationBrowserViewModel.cs index 52439a0c9b7..18c77a4cd98 100644 --- a/src/DocumentationBrowserViewExtension/DocumentationBrowserViewModel.cs +++ b/src/DocumentationBrowserViewExtension/DocumentationBrowserViewModel.cs @@ -75,7 +75,7 @@ private set if (value != oldLink) { UnsubscribeMdWatcher(); - WatchMdFile(value.OriginalString); + WatchMdFile(value?.OriginalString, CurrentGraphName, CurrentPackageName); } this.link = value; @@ -298,7 +298,7 @@ private string GetGraphLinkFromMDLocation(Uri link,bool isOwnedByPackage) } } - private bool IsBuiltInDocPath(string mdLink) + internal bool IsBuiltInDocPath(string mdLink) { if (string.IsNullOrEmpty(mdLink)) return false; @@ -327,13 +327,12 @@ private bool IsBuiltInDocPath(string mdLink) } } - private void WatchMdFile(string mdLink) + private void WatchMdFile(string mdLink, string nodeNamespace, string packageName) { if (string.IsNullOrWhiteSpace(mdLink)) return; - var fileName = Path.GetFileNameWithoutExtension(mdLink); - if (!packageManagerDoc.ContainsAnnotationDoc(fileName)) + if (!packageManagerDoc.ContainsAnnotationDoc(nodeNamespace ?? string.Empty, packageName ?? string.Empty)) return; markdownFileWatcher = new FileSystemWatcher(Path.GetDirectoryName(mdLink)) @@ -493,7 +492,7 @@ internal void InsertGraph() internal delegate void InsertDocumentationLinkEventHandler(object sender, InsertDocumentationLinkEventArgs e); internal event InsertDocumentationLinkEventHandler HandleInsertFile; - private string DynamoGraphFromMDFilePath(string path, bool IsOwnedByPackage) + internal string DynamoGraphFromMDFilePath(string path, bool IsOwnedByPackage) { path = HttpUtility.UrlDecode(path); if (!IsOwnedByPackage) diff --git a/src/DocumentationBrowserViewExtension/PackageDocumentationManager.cs b/src/DocumentationBrowserViewExtension/PackageDocumentationManager.cs index 5b5b74557de..5a89d1f7b22 100644 --- a/src/DocumentationBrowserViewExtension/PackageDocumentationManager.cs +++ b/src/DocumentationBrowserViewExtension/PackageDocumentationManager.cs @@ -27,6 +27,11 @@ public class PackageDocumentationManager : IDisposable /// private Dictionary markdownFileWatchers = new Dictionary(); + /// + /// Map of package name to its node documentation directory path. Used to resolve hash-named doc files in packages. + /// + private Dictionary packageDocDirectories = new Dictionary(); + private const string VALID_DOC_FILEEXTENSION = "*.md"; private const string FALLBACK_DOC_DIRECTORY_NAME = "fallback_docs"; private static PackageDocumentationManager instance; @@ -97,8 +102,24 @@ public string GetAnnotationDoc(string nodeNamespace, string packageName) } var shortName = Hash.GetHashFilenameFromString(nodeNamespace); - FileInfo matchingDoc = null; + + // Try hash-named file in package doc directory (packages may use hash filenames for long paths) + if (!string.IsNullOrEmpty(packageName) && packageDocDirectories.TryGetValue(packageName, out var pkgDocPath)) + { + var pkgDir = new DirectoryInfo(pkgDocPath); + if (pkgDir.Exists) + { + matchingDoc = pkgDir.GetFiles($"{shortName}.md", SearchOption.AllDirectories).FirstOrDefault(); + if (matchingDoc != null) + { + // Cache so future lookups and file watcher logic resolve correctly + nodeDocumentationFileLookup[Path.Combine(packageName, nodeNamespace)] = matchingDoc.FullName; + return matchingDoc.FullName; + } + } + } + if (hostDynamoFallbackDocPath != null) { matchingDoc = hostDynamoFallbackDocPath.GetFiles($"{shortName}.md").FirstOrDefault() ?? @@ -119,13 +140,14 @@ public string GetAnnotationDoc(string nodeNamespace, string packageName) } /// - /// Checks if the nodeNamespace has a documentation markdown associated. + /// Checks if the node has a documentation markdown associated (by direct lookup or hash-named file in package/fallback). /// - /// - /// - public bool ContainsAnnotationDoc(string nodeNamespace) + /// Namespace (e.g. minimum qualified name) of the node. + /// Package name if the node is from a package; otherwise empty. + /// True if documentation can be resolved for this node. + public bool ContainsAnnotationDoc(string nodeNamespace, string packageName) { - return nodeDocumentationFileLookup.ContainsKey(nodeNamespace); + return !string.IsNullOrEmpty(GetAnnotationDoc(nodeNamespace, packageName)); } /// @@ -143,6 +165,7 @@ internal void AddPackageDocumentation(string packageDocumentationPath, string pa return; MonitorDirectory(directoryInfo); + packageDocDirectories[packageName] = directoryInfo.FullName; var files = directoryInfo.GetFiles(VALID_DOC_FILEEXTENSION, SearchOption.AllDirectories); TrackDocumentationFiles(files, packageName); } @@ -165,7 +188,8 @@ private void TrackDocumentationFiles(FileInfo[] files, string packageName) { try { - nodeDocumentationFileLookup.Add(Path.Combine(packageName,Path.GetFileNameWithoutExtension(file.Name)), file.FullName); + var key = Path.Combine(packageName, Path.GetFileNameWithoutExtension(file.Name)); + nodeDocumentationFileLookup[key] = file.FullName; } catch (Exception e) { diff --git a/src/DocumentationBrowserViewExtension/Properties/Resources.Designer.cs b/src/DocumentationBrowserViewExtension/Properties/Resources.Designer.cs index 00db91e475d..cc3dde00b52 100644 --- a/src/DocumentationBrowserViewExtension/Properties/Resources.Designer.cs +++ b/src/DocumentationBrowserViewExtension/Properties/Resources.Designer.cs @@ -177,6 +177,87 @@ public static string MenuItemText { } } + /// + /// Looks up a localized string similar to Node help audit CSV saved to: {0}. + /// + public static string NodeHelpAuditCompleted { + get { + return ResourceManager.GetString("NodeHelpAuditCompleted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Library,Category,Name,FullName,MissingMd,MissingDyn,MarkdownPath,SampleGraphPath. + /// + public static string NodeHelpAuditCsvHeader { + get { + return ResourceManager.GetString("NodeHelpAuditCsvHeader", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Node help audit skipped '{0}': {1}. + /// + public static string NodeHelpAuditEntryFailed { + get { + return ResourceManager.GetString("NodeHelpAuditEntryFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Node help audit failed: {0}. + /// + public static string NodeHelpAuditFailed { + get { + return ResourceManager.GetString("NodeHelpAuditFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Node help audit failed: documentation manager is not available.. + /// + public static string NodeHelpAuditManagerMissing { + get { + return ResourceManager.GetString("NodeHelpAuditManagerMissing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Node help audit found no search entries to process.. + /// + public static string NodeHelpAuditNoEntries { + get { + return ResourceManager.GetString("NodeHelpAuditNoEntries", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Node help audit is not available yet.. + /// + public static string NodeHelpAuditNotReady { + get { + return ResourceManager.GetString("NodeHelpAuditNotReady", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CSV (*.csv)|*.csv. + /// + public static string NodeHelpAuditSaveDialogFilter { + get { + return ResourceManager.GetString("NodeHelpAuditSaveDialogFilter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save Node Help Audit CSV. + /// + public static string NodeHelpAuditSaveDialogTitle { + get { + return ResourceManager.GetString("NodeHelpAuditSaveDialogTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Message. /// diff --git a/src/DocumentationBrowserViewExtension/Properties/Resources.resx b/src/DocumentationBrowserViewExtension/Properties/Resources.resx index af6cad8a203..de68927e637 100644 --- a/src/DocumentationBrowserViewExtension/Properties/Resources.resx +++ b/src/DocumentationBrowserViewExtension/Properties/Resources.resx @@ -222,4 +222,31 @@ Message + + Node help audit CSV saved to: {0} + + + Library,Category,Name,FullName,MissingMd,MissingDyn,MissingImage,MarkdownPath,SampleGraphPath,ImagePaths + + + Node help audit skipped '{0}': {1} + + + Node help audit failed: {0} + + + Node help audit failed: documentation manager is not available. + + + Node help audit found no search entries to process. + + + Node help audit is not available yet. + + + CSV (*.csv)|*.csv + + + Save Node Help Audit CSV + \ No newline at end of file diff --git a/src/DynamoCoreWpf/Extensions/ViewLoadedParams.cs b/src/DynamoCoreWpf/Extensions/ViewLoadedParams.cs index 3acd33c21b5..77d2ce7af09 100644 --- a/src/DynamoCoreWpf/Extensions/ViewLoadedParams.cs +++ b/src/DynamoCoreWpf/Extensions/ViewLoadedParams.cs @@ -210,6 +210,16 @@ public event RequestOpenDocumentationLinkHandler RequestOpenDocumentationLink } } + /// + /// Event raised when the user requests a node help documentation audit (e.g. from Debug menu). + /// Extensions (e.g. Documentation Browser) should subscribe to this event to run the audit. + /// + public event Action NodeHelpAuditRequested + { + add => dynamoViewModel.NodeHelpAuditRequested += value; + remove => dynamoViewModel.NodeHelpAuditRequested -= value; + } + /// /// Request to open a view extension in the side panel by name. /// diff --git a/src/DynamoCoreWpf/Properties/Resources.Designer.cs b/src/DynamoCoreWpf/Properties/Resources.Designer.cs index 3578fcad1ab..44fc177496b 100644 --- a/src/DynamoCoreWpf/Properties/Resources.Designer.cs +++ b/src/DynamoCoreWpf/Properties/Resources.Designer.cs @@ -1550,6 +1550,15 @@ public static string DynamoViewDebugMenuDebugModes { } } + /// + /// Looks up a localized string similar to Audit Node Help Docs (CSV). + /// + public static string DynamoViewDebugMenuAuditNodeHelpDocs { + get { + return ResourceManager.GetString("DynamoViewDebugMenuAuditNodeHelpDocs", resourceCulture); + } + } + /// /// Looks up a localized string similar to _Dump Data. /// diff --git a/src/DynamoCoreWpf/Properties/Resources.en-US.resx b/src/DynamoCoreWpf/Properties/Resources.en-US.resx index e6a28d6ede2..6898c2d5dcb 100644 --- a/src/DynamoCoreWpf/Properties/Resources.en-US.resx +++ b/src/DynamoCoreWpf/Properties/Resources.en-US.resx @@ -503,6 +503,10 @@ Don't worry, you'll have the option to save your work. Debug _Modes Debug menu | Show debug modes + + Audit Node Help Docs (CSV) + Debug menu | Audit node help documentation + _Edit Edit menu diff --git a/src/DynamoCoreWpf/Properties/Resources.resx b/src/DynamoCoreWpf/Properties/Resources.resx index 9332700deeb..45b322e26bd 100644 --- a/src/DynamoCoreWpf/Properties/Resources.resx +++ b/src/DynamoCoreWpf/Properties/Resources.resx @@ -254,6 +254,10 @@ Debug _Modes Debug menu | Show debug modes + + Audit Node Help Docs (CSV) + Debug menu | Audit node help documentation + _Edit Edit menu diff --git a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelEvents.cs b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelEvents.cs index 4bfd6b6b491..6ec581a56aa 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelEvents.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelEvents.cs @@ -98,6 +98,16 @@ public virtual void OnRequestOpenDocumentationLink(OpenDocumentationLinkEventArg RequestOpenDocumentationLink?.Invoke(e); } + /// + /// Event raised when the user requests a node help documentation audit (e.g. from Debug menu). + /// The Documentation Browser extension subscribes to this event to run the audit. + /// + internal event Action NodeHelpAuditRequested; + internal virtual void OnNodeHelpAuditRequested() + { + NodeHelpAuditRequested?.Invoke(); + } + public event Action RequestShowHideSidebar; public virtual void OnShowHideSidebar(bool show) { diff --git a/src/DynamoCoreWpf/Views/Core/DynamoView.xaml b/src/DynamoCoreWpf/Views/Core/DynamoView.xaml index 4aeb02b752c..c129d35e0c5 100644 --- a/src/DynamoCoreWpf/Views/Core/DynamoView.xaml +++ b/src/DynamoCoreWpf/Views/Core/DynamoView.xaml @@ -697,6 +697,10 @@ Header="{x:Static p:Resources.DynamoViewDebugMenuDumpNodeIcons}" IsEnabled="True" /> +