diff --git a/PowerDocu.FlowDocumenter/FlowDocumentationContent.cs b/PowerDocu.FlowDocumenter/FlowDocumentationContent.cs new file mode 100644 index 0000000..3116207 --- /dev/null +++ b/PowerDocu.FlowDocumenter/FlowDocumentationContent.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using PowerDocu.Common; + +namespace PowerDocu.FlowDocumenter +{ + class FlowDocumentationContent + { + public string folderPath, filename; + public FlowMetadata metadata; + public FlowOverview overview; + public FlowConnectionReferences connectionReferences; + public FlowTrigger trigger; + public FlowVariables variables; + public FlowDetails details; + public FlowActions actions; + + public FlowDocumentationContent(FlowEntity flow, string path) + { + folderPath = path + CharsetHelper.GetSafeName(@"\FlowDoc - " + flow.Name + @"\"); + filename = CharsetHelper.GetSafeName(flow.Name) + ((flow.ID != null) ? ("(" + flow.ID + ")") : ""); + metadata = new FlowMetadata(flow); + overview = new FlowOverview(); + connectionReferences = new FlowConnectionReferences(flow); + trigger = new FlowTrigger(flow); + variables = new FlowVariables(flow); + details = new FlowDetails(); + actions = new FlowActions(flow); + NotificationHelper.SendNotification("Preparing documentation content for " + flow.Name); + } + } + + public class FlowMetadata + { + public string Name; + public string ID; + public string header; + public Dictionary metadataTable; + + public FlowMetadata(FlowEntity flow) + { + ID = flow.ID; + Name = flow.Name; + header = "Flow Documentation - " + Name; + metadataTable = new Dictionary + { + { "Flow Name", flow.Name } + }; + if (!String.IsNullOrEmpty(flow.ID)) + { + metadataTable.Add("Flow ID", flow.ID); + } + metadataTable.Add("Documentation generated at", DateTime.Now.ToLongDateString() + " " + DateTime.Now.ToShortTimeString()); + metadataTable.Add("Number of Variables", "" + flow.actions.ActionNodes.Count(o => o.Type == "InitializeVariable")); + metadataTable.Add("Number of Actions", "" + flow.actions.ActionNodes.Count); + } + } + + public class FlowOverview + { + public string header; + public string infoText; + public string pngFile = "flow.png"; + public string svgFile = "flow.svg"; + + public FlowOverview() + { + header = "Flow Overview"; + infoText = "The following chart shows the top level layout of the Flow. For a detailed view, please visit the section called Detailed Flow Diagram"; + } + } + + public class FlowConnectionReferences + { + public string header; + public string infoText; + public Dictionary> connectionTable; + public FlowConnectionReferences(FlowEntity flow) + { + header = "Connections"; + infoText = $"There are a total of {flow.connectionReferences.Count} connections used in this Flow:"; + connectionTable = new Dictionary>(); + foreach (ConnectionReference cRef in flow.connectionReferences) + { + string connectorUniqueName = cRef.Name; + Dictionary connectionDetailsTable = new Dictionary + { + { "Connection Type", cRef.Type.ToString() } + }; + if (cRef.Type == ConnectionType.ConnectorReference) + { + if (!String.IsNullOrEmpty(cRef.ConnectionReferenceLogicalName)) + connectionDetailsTable.Add("Connection Reference Name", cRef.ConnectionReferenceLogicalName); + } + if (!String.IsNullOrEmpty(cRef.ID)) + { + connectionDetailsTable.Add("ID", cRef.ID); + } + if (!String.IsNullOrEmpty(cRef.Source)) + { + connectionDetailsTable.Add("Source", cRef.Source); + } + connectionTable.TryAdd(connectorUniqueName, connectionDetailsTable); + } + } + } + + public class FlowTrigger + { + public string header; + public string infoText; + public Dictionary triggerTable; + public List inputs; + public string inputsHeader = "Inputs Details"; + public List triggerProperties; + public string triggerPropertiesHeader = "Other Trigger Properties"; + public FlowTrigger(FlowEntity flow) + { + header = "Trigger"; + + triggerTable = new Dictionary + { + { "Name", flow.trigger.Name }, + { "Type", flow.trigger.Type } + }; + if (!String.IsNullOrEmpty(flow.trigger.Connector)) + { + triggerTable.Add("Connector", flow.trigger.Connector); + } + //Description = a Note added + if (!String.IsNullOrEmpty(flow.trigger.Description)) + { + triggerTable.Add("Description / Note", flow.trigger.Description); + } + if (flow.trigger.Recurrence.Count > 0) + { + triggerTable.Add("Recurrence Details", "mergedrow"); + foreach (KeyValuePair properties in flow.trigger.Recurrence) + { + triggerTable.Add(properties.Key, properties.Value); + } + } + if (flow.trigger.Inputs.Count > 0) + { + inputs = flow.trigger.Inputs; + } + if (flow.trigger.TriggerProperties.Count > 0) + { + triggerProperties = flow.trigger.TriggerProperties; + } + } + } + + public class FlowVariables + { + public string header = "Variables"; + public Dictionary> variablesTable; + public Dictionary> referencesTable; + public Dictionary> initialValTable; + public FlowVariables(FlowEntity flow) + { + variablesTable = new Dictionary>(); + referencesTable = new Dictionary>(); + initialValTable = new Dictionary>(); + List variablesNodes = flow.actions.ActionNodes.Where(o => o.Type == "InitializeVariable").OrderBy(o => o.Name).ToList(); + List modifyVariablesNodes = flow.actions.ActionNodes.Where(o => o.Type == "SetVariable" || o.Type == "IncrementVariable").ToList(); + foreach (ActionNode node in variablesNodes) + { + foreach (Expression exp in node.actionInputs) + { + if (exp.expressionOperator == "variables") + { + Dictionary variableValueTable = new Dictionary(); + string vname = ""; + string vtype = ""; + foreach (Expression expO in exp.expressionOperands) + { + if (expO.expressionOperator == "name") + { + vname = expO.expressionOperands[0].ToString(); + } + if (expO.expressionOperator == "type") + { + vtype = expO.expressionOperands[0].ToString(); + } + if (expO.expressionOperator == "value") + { + if (expO.expressionOperands.Count == 1) + { + variableValueTable.Add(expO.expressionOperands[0].ToString(), ""); + } + else + { + foreach (var eop in expO.expressionOperands) + { + if (eop.GetType() == typeof(string)) + { + variableValueTable.Add(eop.ToString(), ""); + } + else + { + variableValueTable.Add(((Expression)eop).expressionOperator, + (((Expression)eop).expressionOperands.Count > 0) ? ((Expression)eop).expressionOperands[0].ToString() : ""); + } + } + } + } + } + List referencedInNodes = new List + { + node + }; + foreach (ActionNode actionNode in modifyVariablesNodes) + { + foreach (Expression expO in actionNode.actionInputs) + { + if (expO.expressionOperator == "name") + { + if (expO.expressionOperands[0].ToString().Equals(vname)) + { + referencedInNodes.Add(actionNode); + } + } + } + } + foreach (ActionNode actionNode in flow.actions.ActionNodes) + { + if (actionNode.actionExpression?.ToString().Contains($"@variables('{vname}')") == true + || actionNode.Expression?.Contains($"@variables('{vname}')") == true + || actionNode.actionInput?.ToString().Contains($"@variables('{vname}')") == true) + { + referencedInNodes.Add(actionNode); + } + else + { + foreach (Expression actionInput in actionNode.actionInputs) + { + if (actionInput.ToString().Contains($"@variables('{vname}')")) + { + referencedInNodes.Add(actionNode); + } + } + } + } + + Dictionary variableDetailsTable = new Dictionary + { + { "Name", vname }, + {"Type",vtype}, + {"Initial Value", ""} + }; + variablesTable.Add(vname, variableDetailsTable); + referencesTable.Add(vname, referencedInNodes); + initialValTable.Add(vname, variableValueTable); + } + } + } + } + } + + public class FlowDetails + { + public string header = "Detailed Flow Diagram"; + public string infoText = "The following chart shows the detailed layout of the Flow"; + public string imageFileName = "flow-detailed"; + } + + public class FlowActions + { + public string header = "Actions"; + public string infoText = ""; + public Dictionary> actionsTable; + public List actionNodesList; + public FlowActions(FlowEntity flow) + { + actionsTable = new Dictionary>(); + actionNodesList = flow.actions.ActionNodes.OrderBy(o => o.Name).ToList(); + infoText = $"There are a total of {actionNodesList.Count} actions used in this Flow:"; + } + } +} \ No newline at end of file diff --git a/PowerDocu.FlowDocumenter/FlowDocumentationGenerator.cs b/PowerDocu.FlowDocumenter/FlowDocumentationGenerator.cs index 8e221a8..8cf7b43 100644 --- a/PowerDocu.FlowDocumenter/FlowDocumentationGenerator.cs +++ b/PowerDocu.FlowDocumenter/FlowDocumentationGenerator.cs @@ -6,7 +6,7 @@ namespace PowerDocu.FlowDocumenter { public static class FlowDocumentationGenerator { - public static void GenerateWordDocumentation(string filePath, string wordTemplate = null) + public static void GenerateDocumentation(string filePath, string fileFormat, string wordTemplate = null) { if (File.Exists(filePath)) { @@ -22,17 +22,27 @@ public static void GenerateWordDocumentation(string filePath, string wordTemplat GraphBuilder gbzip = new GraphBuilder(flow, path); gbzip.buildTopLevelGraph(); gbzip.buildDetailedGraph(); - if (String.IsNullOrEmpty(wordTemplate) || !File.Exists(wordTemplate)) + FlowDocumentationContent content = new FlowDocumentationContent(flow, path); + if (fileFormat.Equals(OutputFormatHelper.Word) || fileFormat.Equals(OutputFormatHelper.All)) { - FlowWordDocBuilder wordzip = new FlowWordDocBuilder(flow, path, null); + NotificationHelper.SendNotification("Creating Word documentation"); + if (String.IsNullOrEmpty(wordTemplate) || !File.Exists(wordTemplate)) + { + FlowWordDocBuilder wordzip = new FlowWordDocBuilder(content, null); + } + else + { + FlowWordDocBuilder wordzip = new FlowWordDocBuilder(content, wordTemplate); + } } - else + if (fileFormat.Equals(OutputFormatHelper.Markdown) || fileFormat.Equals(OutputFormatHelper.All)) { - FlowWordDocBuilder wordzip = new FlowWordDocBuilder(flow, path, wordTemplate); + NotificationHelper.SendNotification("Creating Markdown documentation"); + FlowMarkdownBuilder markdownFile = new FlowMarkdownBuilder(content); } } DateTime endDocGeneration = DateTime.Now; - NotificationHelper.SendNotification("FlowDocumenter: Created Word documentation for " + filePath + ". A total of " + flowParserFromZip.getFlows().Count + " files were processed in " + (endDocGeneration - startDocGeneration).TotalSeconds + " seconds."); + NotificationHelper.SendNotification("FlowDocumenter: Created documentation for " + filePath + ". A total of " + flowParserFromZip.getFlows().Count + " files were processed in " + (endDocGeneration - startDocGeneration).TotalSeconds + " seconds."); } else { diff --git a/PowerDocu.FlowDocumenter/FlowDocumenter.cs b/PowerDocu.FlowDocumenter/FlowDocumenter.cs index 7e41ef0..27512dd 100644 --- a/PowerDocu.FlowDocumenter/FlowDocumenter.cs +++ b/PowerDocu.FlowDocumenter/FlowDocumenter.cs @@ -18,9 +18,9 @@ static void Main(string[] args) else { if (args.Length == 1) - FlowDocumentationGenerator.GenerateWordDocumentation(args[0]); + FlowDocumentationGenerator.GenerateDocumentation(args[0], "All"); if (args.Length == 2) - FlowDocumentationGenerator.GenerateWordDocumentation(args[0], args[1]); + FlowDocumentationGenerator.GenerateDocumentation(args[0], "All", args[1]); } } } diff --git a/PowerDocu.FlowDocumenter/FlowMarkdownBuilder.cs b/PowerDocu.FlowDocumenter/FlowMarkdownBuilder.cs new file mode 100644 index 0000000..c2038ca --- /dev/null +++ b/PowerDocu.FlowDocumenter/FlowMarkdownBuilder.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using PowerDocu.Common; +using Grynwald.MarkdownGenerator; + +namespace PowerDocu.FlowDocumenter +{ + class FlowMarkdownBuilder : MarkdownBuilder + { + private readonly MdDocument mainDocument; + private readonly string mainDocumentFileName; + private readonly MdDocument connectionsDocument; + private readonly string connectionsDocumentFileName; + private readonly MdDocument variablesDocument; + private readonly string variablesDocumentFileName; + private readonly MdDocument triggerActionsDocument; + private readonly string triggerActionsFileName; + private readonly FlowDocumentationContent content; + + public FlowMarkdownBuilder(FlowDocumentationContent contentdocumentation) + { + content = contentdocumentation; + Directory.CreateDirectory(content.folderPath); + mainDocumentFileName = ("index " + content.filename + ".md").Replace(" ", "-"); + connectionsDocumentFileName = ("connections " + content.filename + ".md").Replace(" ", "-"); + variablesDocumentFileName = ("variables " + content.filename + ".md").Replace(" ", "-"); + triggerActionsFileName = ("triggersactions " + content.filename + ".md").Replace(" ", "-"); + var set = new DocumentSet(); + mainDocument = set.CreateMdDocument(mainDocumentFileName); + connectionsDocument = set.CreateMdDocument(connectionsDocumentFileName); + variablesDocument = set.CreateMdDocument(variablesDocumentFileName); + triggerActionsDocument = set.CreateMdDocument(triggerActionsFileName); + + //add all the relevant content + addFlowMetadata(); + addFlowOverview(); + addConnectionReferenceInfo(); + addTriggerInfo(); + addVariablesInfo(); + addActionInfo(); + addFlowDetails(); + set.Save(content.folderPath); + NotificationHelper.SendNotification("Created Markdown documentation for " + content.metadata.Name); + } + + private MdBulletList getNavigationLinks() + { + MdListItem[] navItems = new MdListItem[] { + new MdListItem(new MdLinkSpan("Overview", mainDocumentFileName)), + new MdListItem(new MdLinkSpan("Connection References",connectionsDocumentFileName)), + new MdListItem(new MdLinkSpan("Variables", variablesDocumentFileName)), + new MdListItem(new MdLinkSpan("Triggers & Actions", triggerActionsFileName)) + }; + return new MdBulletList(navItems); + } + + private void addFlowMetadata() + { + List tableRows = new List(); + foreach (KeyValuePair kvp in content.metadata.metadataTable) + { + tableRows.Add(new MdTableRow(kvp.Key, kvp.Value)); + } + MdTable table = new MdTable(new MdTableRow(new List() { "Flow Name", content.metadata.Name }), tableRows); + // prepare the common sections for all documents + mainDocument.Root.Add(new MdHeading(content.metadata.header, 1)); + mainDocument.Root.Add(table); + mainDocument.Root.Add(getNavigationLinks()); + connectionsDocument.Root.Add(new MdHeading(content.metadata.header, 1)); + connectionsDocument.Root.Add(table); + connectionsDocument.Root.Add(getNavigationLinks()); + variablesDocument.Root.Add(new MdHeading(content.metadata.header, 1)); + variablesDocument.Root.Add(table); + variablesDocument.Root.Add(getNavigationLinks()); + triggerActionsDocument.Root.Add(new MdHeading(content.metadata.header, 1)); + triggerActionsDocument.Root.Add(table); + triggerActionsDocument.Root.Add(getNavigationLinks()); + } + + private void addFlowOverview() + { + mainDocument.Root.Add(new MdHeading(content.overview.header, 2)); + mainDocument.Root.Add(new MdParagraph(new MdTextSpan(content.overview.infoText))); + mainDocument.Root.Add(new MdParagraph(new MdImageSpan("Flow Overview Diagram", content.overview.svgFile))); + } + + private void addConnectionReferenceInfo() + { + connectionsDocument.Root.Add(new MdHeading(content.connectionReferences.header, 2)); + connectionsDocument.Root.Add(new MdParagraph(new MdTextSpan(content.connectionReferences.infoText))); + foreach (KeyValuePair> kvp in content.connectionReferences.connectionTable) + { + string connectorUniqueName = kvp.Key; + ConnectorIcon connectorIcon = ConnectorHelper.getConnectorIcon(connectorUniqueName); + connectionsDocument.Root.Add(new MdHeading((connectorIcon != null) ? connectorIcon.Name : connectorUniqueName, 3)); + + List tableRows = new List(); + foreach (KeyValuePair kvp2 in kvp.Value) + { + tableRows.Add(new MdTableRow(kvp2.Key, kvp2.Value)); + } + MdTable table = new MdTable(new MdTableRow(new MdTextSpan("Connector"), getConnectorNameAndIcon(connectorUniqueName, "https://docs.microsoft.com/connectors/" + connectorUniqueName)), tableRows); //todo: + connectionsDocument.Root.Add(table); + } + } + + private MdLinkSpan getConnectorNameAndIcon(string connectorUniqueName, string url) + { + ConnectorIcon connectorIcon = ConnectorHelper.getConnectorIcon(connectorUniqueName); + if (ConnectorHelper.getConnectorIconFile(connectorUniqueName) != "") + { + return new MdLinkSpan(new MdCompositeSpan( + new MdImageSpan(connectorUniqueName, connectorUniqueName + "32.png"), + new MdTextSpan(" " + ((connectorIcon != null) ? connectorIcon.Name : connectorUniqueName)) + ), url); + } + else + { + return new MdLinkSpan((connectorIcon != null) ? connectorIcon.Name : connectorUniqueName, url); + } + } + + private void addVariablesInfo() + { + variablesDocument.Root.Add(new MdHeading(content.variables.header, 2)); + foreach (KeyValuePair> kvp in content.variables.variablesTable) + { + variablesDocument.Root.Add(new MdHeading(kvp.Key, 3)); + + List tableRows = new List(); + + foreach (KeyValuePair kvp2 in kvp.Value) + { + if (!kvp2.Key.Equals("Initial Value")) + { + tableRows.Add(new MdTableRow(kvp2.Key, kvp2.Value)); + } + } + MdTable table = new MdTable(new MdTableRow(new List() { "Property", "Value" }), tableRows); + variablesDocument.Root.Add(table); + if (kvp.Value.ContainsKey("Initial Value")) + { + tableRows = new List(); + content.variables.initialValTable.TryGetValue(kvp.Key, out Dictionary initialValues); + foreach (KeyValuePair initialVal in initialValues) + { + tableRows.Add(new MdTableRow(initialVal.Key, initialVal.Value)); + } + if (tableRows.Count > 0) + { + table = new MdTable(new MdTableRow(new List() { "Variable Property", "Initial Value" }), tableRows); + variablesDocument.Root.Add(table); + } + } + content.variables.referencesTable.TryGetValue(kvp.Key, out List references); + if (references?.Count > 0) + { + tableRows = new List(); + foreach (ActionNode action in references.OrderBy(o => o.Name).ToList()) + { + tableRows.Add(new MdTableRow(new MdLinkSpan(action.Name, triggerActionsFileName + getLinkFromText(action.Name)))); + } + table = new MdTable(new MdTableRow(new List() { "Variable Used In" }), tableRows); + variablesDocument.Root.Add(table); + } + } + } + + private void addTriggerInfo() + { + triggerActionsDocument.Root.Add(new MdHeading(content.trigger.header, 2)); + List tableRows = new List(); + foreach (KeyValuePair kvp in content.trigger.triggerTable) + { + if (kvp.Value == "mergedrow") + { + tableRows.Add(new MdTableRow(new MdCompositeSpan(kvp.Key))); + } + else + { + tableRows.Add(new MdTableRow(kvp.Key, kvp.Value)); + } + } + MdTable table = new MdTable(new MdTableRow(new List() { "Property", "Value" }), tableRows); + triggerActionsDocument.Root.Add(table); + if (content.trigger.inputs?.Count > 0) + { + triggerActionsDocument.Root.Add(new MdHeading(content.trigger.inputsHeader, 3)); + triggerActionsDocument.Root.Add(new MdParagraph(new MdRawMarkdownSpan(AddExpressionDetails(content.trigger.inputs)))); + } + if (content.trigger.triggerProperties?.Count > 0) + { + triggerActionsDocument.Root.Add(new MdHeading("Other Trigger Properties", 3)); + triggerActionsDocument.Root.Add(new MdParagraph(new MdRawMarkdownSpan(AddExpressionDetails(content.trigger.triggerProperties)))); + } + } + + private void addActionInfo() + { + List actionNodesList = content.actions.actionNodesList; + triggerActionsDocument.Root.Add(new MdHeading(content.actions.header, 2)); + triggerActionsDocument.Root.Add(new MdParagraph(new MdTextSpan(content.actions.infoText))); + + foreach (ActionNode action in actionNodesList) + { + triggerActionsDocument.Root.Add(new MdHeading(action.Name, 3)); + List tableRows = new List + { + new MdTableRow("Name", action.Name), + new MdTableRow("Type", action.Type) + }; + if (!String.IsNullOrEmpty(action.Description)) + { + tableRows.Add(new MdTableRow("Description / Note", action.Description)); + } + if (!String.IsNullOrEmpty(action.Connection)) + { + tableRows.Add(new MdTableRow("Connection", + getConnectorNameAndIcon(action.Connection, "https://docs.microsoft.com/connectors/" + action.Connection))); + } + + //TODO provide more details, such as information about subaction, subsequent actions, switch actions, ... + if (action.actionExpression != null || !String.IsNullOrEmpty(action.Expression)) + { + tableRows.Add(new MdTableRow("Expression", new MdRawMarkdownSpan((action.actionExpression != null) ? AddExpressionTable(action.actionExpression).ToString() : action.Expression))); + } + MdTable table = new MdTable(new MdTableRow(new List() { "Property", "Value" }), tableRows); + triggerActionsDocument.Root.Add(table); + if (action.actionInputs.Count > 0 || !String.IsNullOrEmpty(action.Inputs)) + { + tableRows = new List(); + triggerActionsDocument.Root.Add(new MdHeading("Inputs", 4)); + if (action.actionInputs.Count > 0) + { + tableRows.Add(new MdTableRow("test", "test")); + foreach (Expression actionInput in action.actionInputs) + { + StringBuilder operandsCell = new StringBuilder(); + if (actionInput.expressionOperands.Count > 1) + { + StringBuilder operandsTable = new StringBuilder(""); + foreach (object actionInputOperand in actionInput.expressionOperands) + { + if (actionInputOperand.GetType() == typeof(Expression)) + { + operandsTable.Append(AddExpressionTable((Expression)actionInputOperand, false)); + } + else + { + operandsTable.Append(""); + } + } + operandsCell.Append(operandsTable.Append("
").Append(actionInputOperand.ToString()).Append("
")); + } + else + { + if (actionInput.expressionOperands.Count == 0) + { + operandsCell.Append(""); + } + else + { + if (actionInput.expressionOperands[0]?.GetType() == typeof(Expression)) + { + operandsCell.Append(AddExpressionTable((Expression)actionInput.expressionOperands[0])); + } + else + { + operandsCell.Append(actionInput.expressionOperands[0]?.ToString()); + } + } + } + tableRows.Add(new MdTableRow(actionInput.expressionOperator, new MdRawMarkdownSpan(operandsCell.ToString()))); + } + } + if (!String.IsNullOrEmpty(action.Inputs)) + { + tableRows.Add(new MdTableRow("Value", action.Inputs)); + } + table = new MdTable(new MdTableRow(new List() { "Property", "Value" }), tableRows); + triggerActionsDocument.Root.Add(table); + } + + if (action.Subactions.Count > 0 || action.Elseactions.Count > 0) + { + if (action.Subactions.Count > 0) + { + tableRows = new List(); + triggerActionsDocument.Root.Add(new MdHeading(action.Type == "Switch" ? "Switch Actions" : "Subactions", 4)); + if (action.Type == "Switch") + { + foreach (ActionNode subaction in action.Subactions) + { + if (action.switchRelationship.TryGetValue(subaction, out string switchValue)) + { + tableRows.Add(new MdTableRow(switchValue, new MdLinkSpan(subaction.Name, getLinkFromText(subaction.Name)))); + } + } + table = new MdTable(new MdTableRow(new List() { "Case Values", "Action" }), tableRows); + triggerActionsDocument.Root.Add(table); + } + else + { + foreach (ActionNode subaction in action.Subactions) + { + //adding a link to the subaction's section in the documentation + tableRows.Add(new MdTableRow(new MdLinkSpan(subaction.Name, getLinkFromText(subaction.Name)))); + } + table = new MdTable(new MdTableRow(new List() { "Action" }), tableRows); + triggerActionsDocument.Root.Add(table); + } + } + if (action.Elseactions.Count > 0) + { + tableRows = new List(); + triggerActionsDocument.Root.Add(new MdHeading("Elseactions", 4)); + foreach (ActionNode elseaction in action.Elseactions) + { + //adding a link to the elseaction's section + tableRows.Add(new MdTableRow(new MdLinkSpan(elseaction.Name, getLinkFromText(elseaction.Name)))); + } + table = new MdTable(new MdTableRow(new List() { "Elseactions" }), tableRows); + triggerActionsDocument.Root.Add(table); + } + } + if (action.Neighbours.Count > 0) + { + triggerActionsDocument.Root.Add(new MdHeading("Next Action(s) Conditions", 4)); + tableRows = new List(); + foreach (ActionNode nextAction in action.Neighbours) + { + string[] raConditions = action.nodeRunAfterConditions[nextAction]; + tableRows.Add(new MdTableRow(new MdLinkSpan(nextAction.Name + " [" + string.Join(", ", raConditions) + "]", getLinkFromText(nextAction.Name)))); + } + table = new MdTable(new MdTableRow(new List() { "Next Action" }), tableRows); + triggerActionsDocument.Root.Add(table); + } + } + } + + private void addFlowDetails() + { + mainDocument.Root.Add(new MdHeading(content.details.header, 2)); + mainDocument.Root.Add(new MdParagraph(new MdTextSpan(content.details.infoText))); + mainDocument.Root.Add(new MdParagraph(new MdImageSpan(content.details.header, content.details.imageFileName + ".svg"))); + } + } +} \ No newline at end of file diff --git a/PowerDocu.FlowDocumenter/FlowParser.cs b/PowerDocu.FlowDocumenter/FlowParser.cs index 76175bc..c03b4ed 100644 --- a/PowerDocu.FlowDocumenter/FlowParser.cs +++ b/PowerDocu.FlowDocumenter/FlowParser.cs @@ -151,8 +151,6 @@ private void parseConnectionReferences(FlowEntity flow) foreach (JProperty connRef in connectionReferences) { JObject cRefDetails = (JObject)connRef.Value; - //TODO connections work, connection references don't - //if it's a connection reference /* Example: {"shared_commondataserviceforapps": { @@ -286,6 +284,36 @@ private void parseActions(FlowEntity flow, JEnumerable actions, ActionNo break; case "foreach": //TODO + //{"foreach": "@outputs('List_Environment_Capacity_information')?['body/value']"} + //{"foreach": "@body('Get_calendar_view_of_events_(V2)')?['value']"} + //{"foreach": "@items('Apply_to_each_Environment')?['properties/capacity']"} + //{"foreach": "@variables('Apps_Editted')"} + break; + case "runtimeConfiguration": + //TODO + // {"runtimeConfiguration": {"paginationPolicy": {"minimumItemCount": 100000}}} + break; + case "kind": + //TODO + //{"kind": "GetPastTime"} + //{"kind": "PowerApp"} + //{"kind": "Http"} + break; + case "metadata": + //TODO + break; + case "limit": + //TODO + // {"limit": {"count": 240,"timeout": "P10D"}} + //{"limit": {"count": 60,"timeout": "PT1H"}} + break; + case "operationOptions": + //TODO + //{"operationOptions": "DisableAsyncPattern"} + break; + case "trackedProperties": + //TODO + //{"trackedProperties": {"12": 12,"23": 23}} break; default: break; diff --git a/PowerDocu.FlowDocumenter/FlowWordDocBuilder.cs b/PowerDocu.FlowDocumenter/FlowWordDocBuilder.cs index c9de2bc..028d81d 100644 --- a/PowerDocu.FlowDocumenter/FlowWordDocBuilder.cs +++ b/PowerDocu.FlowDocumenter/FlowWordDocBuilder.cs @@ -16,15 +16,13 @@ namespace PowerDocu.FlowDocumenter { class FlowWordDocBuilder : WordDocBuilder { - private readonly FlowEntity flow; + private readonly FlowDocumentationContent content; - public FlowWordDocBuilder(FlowEntity flowToDocument, string path, string template) + public FlowWordDocBuilder(FlowDocumentationContent contentDocumentation, string template) { - this.flow = flowToDocument; - folderPath = path + CharsetHelper.GetSafeName(@"\FlowDoc - " + flow.Name + @"\"); - Directory.CreateDirectory(folderPath); - string filename = CharsetHelper.GetSafeName(flow.Name) + ((flow.ID != null) ? ("(" + flow.ID + ")") : "") + ".docx"; - filename = folderPath + filename; + this.content = contentDocumentation; + Directory.CreateDirectory(content.folderPath); + string filename = content.folderPath + content.filename + ".docx"; InitializeWordDocument(filename, template); using (WordprocessingDocument wordDocument = WordprocessingDocument.Open(filename, true)) { @@ -40,21 +38,21 @@ public FlowWordDocBuilder(FlowEntity flowToDocument, string path, string templat addActionInfo(); addFlowDetails(wordDocument); } - NotificationHelper.SendNotification("Created Word documentation for " + flow.Name); + NotificationHelper.SendNotification("Created Word documentation for " + content.metadata.Name); } private void addConnectionReferenceInfo() { Paragraph para = body.AppendChild(new Paragraph()); Run run = para.AppendChild(new Run()); - run.AppendChild(new Text("Connections")); + run.AppendChild(new Text(content.connectionReferences.header)); ApplyStyleToParagraph("Heading2", para); para = body.AppendChild(new Paragraph()); run = para.AppendChild(new Run()); - run.AppendChild(new Text($"There are a total of {flow.connectionReferences.Count} connections used in this Flow:")); - foreach (ConnectionReference cRef in flow.connectionReferences) + run.AppendChild(new Text(content.connectionReferences.infoText)); + foreach (KeyValuePair> kvp in content.connectionReferences.connectionTable) { - string connectorUniqueName = cRef.Name; + string connectorUniqueName = kvp.Key; ConnectorIcon connectorIcon = ConnectorHelper.getConnectorIcon(connectorUniqueName); para = body.AppendChild(new Paragraph()); run = para.AppendChild(new Run()); @@ -65,19 +63,9 @@ private void addConnectionReferenceInfo() Table table = CreateTable(); table.Append(CreateRow(new Text("Connector"), appendConnectorNameAndIcon(connectorUniqueName, mainPart, rel))); - table.Append(CreateRow(new Text("Connection Type"), new Text(cRef.Type.ToString()))); - if (cRef.Type == ConnectionType.ConnectorReference) + foreach (KeyValuePair kvp2 in kvp.Value) { - if (!String.IsNullOrEmpty(cRef.ConnectionReferenceLogicalName)) - table.Append(CreateRow(new Text("Connection Reference Name"), new Text(cRef.ConnectionReferenceLogicalName))); - } - if (!String.IsNullOrEmpty(cRef.ID)) - { - table.Append(CreateRow(new Text("ID"), new Text(cRef.ID))); - } - if (!String.IsNullOrEmpty(cRef.Source)) - { - table.Append(CreateRow(new Text("Source"), new Text(cRef.Source))); + table.Append(CreateRow(new Text(kvp2.Key), new Text(kvp2.Value))); } body.Append(table); body.AppendChild(new Paragraph(new Run(new Break()))); @@ -123,18 +111,14 @@ private void addFlowMetadata() { Paragraph para = body.AppendChild(new Paragraph()); Run run = para.AppendChild(new Run()); - run.AppendChild(new Text("Flow Documentation - " + flow.Name)); + run.AppendChild(new Text(content.metadata.header)); ApplyStyleToParagraph("Heading1", para); body.AppendChild(new Paragraph(new Run())); Table table = CreateTable(); - table.Append(CreateRow(new Text("Flow Name"), new Text(flow.Name))); - if (!String.IsNullOrEmpty(flow.ID)) + foreach (KeyValuePair kvp in content.metadata.metadataTable) { - table.Append(CreateRow(new Text("Flow ID"), new Text(flow.ID))); + table.Append(CreateRow(new Text(kvp.Key), new Text(kvp.Value))); } - table.Append(CreateRow(new Text("Documentation generated at"), new Text(DateTime.Now.ToLongDateString() + " " + DateTime.Now.ToShortTimeString()))); - table.Append(CreateRow(new Text("Number of Variables"), new Text("" + flow.actions.ActionNodes.Count(o => o.Type == "InitializeVariable")))); - table.Append(CreateRow(new Text("Number of Actions"), new Text("" + flow.actions.ActionNodes.Count))); body.Append(table); body.AppendChild(new Paragraph(new Run(new Break()))); } @@ -143,125 +127,66 @@ private void addVariablesInfo() { Paragraph para = body.AppendChild(new Paragraph()); Run run = para.AppendChild(new Run()); - run.AppendChild(new Text("Variables")); + run.AppendChild(new Text(content.variables.header)); ApplyStyleToParagraph("Heading2", para); para = body.AppendChild(new Paragraph()); run = para.AppendChild(new Run()); - List variablesNodes = flow.actions.ActionNodes.Where(o => o.Type == "InitializeVariable").OrderBy(o => o.Name).ToList(); - List modifyVariablesNodes = flow.actions.ActionNodes.Where(o => o.Type == "SetVariable" || o.Type == "IncrementVariable").ToList(); - foreach (ActionNode node in variablesNodes) + para = body.AppendChild(new Paragraph()); + + foreach (KeyValuePair> kvp in content.variables.variablesTable) { - foreach (Expression exp in node.actionInputs) + para = body.AppendChild(new Paragraph()); + run = para.AppendChild(new Run()); + run.AppendChild(new Text(kvp.Key)); + string bookmarkID = (new Random()).Next(100000, 999999).ToString(); + BookmarkStart start = new BookmarkStart() { Name = CreateMD5Hash(kvp.Key), Id = bookmarkID }; + BookmarkEnd end = new BookmarkEnd() { Id = bookmarkID }; + para.Append(start, end); + ApplyStyleToParagraph("Heading3", para); + + Table table = CreateTable(); + foreach (KeyValuePair kvp2 in kvp.Value) { - if (exp.expressionOperator == "variables") + if (kvp2.Key.Equals("Initial Value")) { - string vname = ""; - string vtype = ""; - OpenXmlElement vval = null; - foreach (Expression expO in exp.expressionOperands) - { - if (expO.expressionOperator == "name") - { - vname = expO.expressionOperands[0].ToString(); - } - if (expO.expressionOperator == "type") - { - vtype = expO.expressionOperands[0].ToString(); - } - if (expO.expressionOperator == "value") - { - if (expO.expressionOperands.Count == 1) - { - vval = new Text(expO.expressionOperands[0].ToString()); - } - else - { - vval = CreateTable(BorderValues.Single, 0.8); - foreach (var eop in expO.expressionOperands) - { - if (eop.GetType() == typeof(string)) - { - vval = new Text(expO.expressionOperands.ToString()); - } - else - { - vval.Append(CreateRow(new Text(((Expression)eop).expressionOperator), - new Text((((Expression)eop).expressionOperands.Count>0) ? ((Expression)eop).expressionOperands[0].ToString() :""))); - } - } - } - } - } - List referencedInNodes = new List - { - node - }; - foreach (ActionNode actionNode in modifyVariablesNodes) - { - foreach (Expression expO in actionNode.actionInputs) - { - if (expO.expressionOperator == "name") - { - if (expO.expressionOperands[0].ToString().Equals(vname)) - { - referencedInNodes.Add(actionNode); - } - } - } - } - foreach (ActionNode actionNode in flow.actions.ActionNodes) + Table initialValTable = CreateTable(BorderValues.Single, 0.8); + content.variables.initialValTable.TryGetValue(kvp.Key, out Dictionary initialValues); + foreach (KeyValuePair initialVal in initialValues) { - if (actionNode.actionExpression?.ToString().Contains($"@variables('{vname}')") == true || actionNode.Expression?.Contains($"@variables('{vname}')") == true || actionNode.actionInput?.ToString().Contains($"@variables('{vname}')") == true) - { - referencedInNodes.Add(actionNode); - } - else - { - foreach (Expression actionInput in actionNode.actionInputs) - { - if (actionInput.ToString().Contains($"@variables('{vname}')")) - { - referencedInNodes.Add(actionNode); - } - } - } + initialValTable.Append(CreateRow(new Text(initialVal.Key), new Text(initialVal.Value))); } - para = body.AppendChild(new Paragraph()); - run = para.AppendChild(new Run()); - run.AppendChild(new Text(vname)); - string bookmarkID = (new Random()).Next(100000, 999999).ToString(); - BookmarkStart start = new BookmarkStart() { Name = CreateMD5Hash(vname), Id = bookmarkID }; - BookmarkEnd end = new BookmarkEnd() { Id = bookmarkID }; - para.Append(start, end); - ApplyStyleToParagraph("Heading3", para); - Table table = CreateTable(); - table.Append(CreateRow(new Text("Name"), new Text(vname))); - table.Append(CreateRow(new Text("Type"), new Text(vtype))); - table.Append(CreateRow(new Text("Initial Value"), (vval == null) ? new Text("") : vval)); - var tr = new TableRow(); - var tc = CreateTableCell(); - run = new Run(new Text("Used in these Actions")); - RunProperties runProperties = new RunProperties(); - runProperties.Append(new Bold()); - run.RunProperties = runProperties; - tc.Append(new Paragraph(run)); - tr.Append(tc); - tc = CreateTableCell(); - foreach (ActionNode action in referencedInNodes.OrderBy(o => o.Name).ToList()) + table.Append(CreateRow(new Text("Initial Value"), initialValTable)); + } + else + { + table.Append(CreateRow(new Text(kvp2.Key), new Text(kvp2.Value))); + } + } + content.variables.referencesTable.TryGetValue(kvp.Key, out List references); + if (references?.Count > 0) + { + var tr = new TableRow(); + var tc = CreateTableCell(); + run = new Run(new Text("Used in these Actions")); + RunProperties runProperties = new RunProperties(); + runProperties.Append(new Bold()); + run.RunProperties = runProperties; + tc.Append(new Paragraph(run)); + tr.Append(tc); + tc = CreateTableCell(); + foreach (ActionNode action in references.OrderBy(o => o.Name).ToList()) + { + tc.Append(new Paragraph(new Hyperlink(new Run(new Text(action.Name))) { - //adding a link to the action's section in the Word doc - tc.Append(new Paragraph(new Hyperlink(new Run(new Text(action.Name))) - { - Anchor = action.Name, - DocLocation = "" - })); - } - tr.Append(tc); - table.Append(tr); - body.Append(table); - body.AppendChild(new Paragraph(new Run(new Break()))); + Anchor = CreateMD5Hash(action.Name), + DocLocation = "" + })); } + tr.Append(tc); + table.Append(tr); } + body.Append(table); + body.AppendChild(new Paragraph(new Run(new Break()))); } } @@ -269,37 +194,29 @@ private void addTriggerInfo() { Paragraph para = body.AppendChild(new Paragraph()); Run run = para.AppendChild(new Run()); - run.AppendChild(new Text("Trigger")); + run.AppendChild(new Text(content.trigger.header)); ApplyStyleToParagraph("Heading2", para); body.AppendChild(new Paragraph(new Run())); Table table = CreateTable(); - table.Append(CreateRow(new Text("Name"), new Text(flow.trigger.Name))); - table.Append(CreateRow(new Text("Type"), new Text(flow.trigger.Type))); - if (!String.IsNullOrEmpty(flow.trigger.Connector)) - { - table.Append(CreateRow(new Text("Connector"), new Text(flow.trigger.Connector))); - } - //Description = a Note added - if (!String.IsNullOrEmpty(flow.trigger.Description)) + foreach (KeyValuePair kvp in content.trigger.triggerTable) { - table.Append(CreateRow(new Text("Description / Note"), new Text(flow.trigger.Description))); - } - if (flow.trigger.Recurrence.Count > 0) - { - table.Append(CreateMergedRow(new Text("Recurrence Details"), 2, cellHeaderBackground)); - foreach (KeyValuePair properties in flow.trigger.Recurrence) + if (kvp.Value == "mergedrow") { - table.Append(CreateRow(new Text(properties.Key), - new Text(properties.Value))); + table.Append(CreateMergedRow(new Text(kvp.Key), 2, cellHeaderBackground)); + } + else + { + table.Append(CreateRow(new Text(kvp.Key), new Text(kvp.Value))); } } - if (flow.trigger.Inputs.Count > 0) + + if (content.trigger.inputs?.Count > 0) { - AddExpressionDetails(table, flow.trigger.Inputs, "Inputs Details"); + AddExpressionDetails(table, content.trigger.inputs, "Inputs Details"); } - if (flow.trigger.TriggerProperties.Count > 0) + if (content.trigger.triggerProperties?.Count > 0) { - AddExpressionDetails(table, flow.trigger.TriggerProperties, "Other Trigger Properties"); + AddExpressionDetails(table, content.trigger.triggerProperties, "Other Trigger Properties"); } body.Append(table); body.AppendChild(new Paragraph(new Run(new Break()))); @@ -317,7 +234,7 @@ private void addFlowOverview(WordprocessingDocument wordDoc) //we generated a png and a svg file. We use both: SVG as the default, PNG as the fallback for older clients that can't display SVG ImagePart imagePart = wordDoc.MainDocumentPart.AddImagePart(ImagePartType.Png); int imageWidth, imageHeight; - using (FileStream stream = new FileStream(folderPath + "flow.png", FileMode.Open)) + using (FileStream stream = new FileStream(content.folderPath + "flow.png", FileMode.Open)) { using (var image = Image.FromStream(stream, false, false)) { @@ -328,7 +245,7 @@ private void addFlowOverview(WordprocessingDocument wordDoc) imagePart.FeedData(stream); } ImagePart svgPart = wordDoc.MainDocumentPart.AddNewPart("image/svg+xml", "rId" + (new Random()).Next(100000, 999999)); - using (FileStream stream = new FileStream(folderPath + "flow.svg", FileMode.Open)) + using (FileStream stream = new FileStream(content.folderPath + "flow.svg", FileMode.Open)) { svgPart.FeedData(stream); } @@ -340,7 +257,7 @@ private void addFlowOverview(WordprocessingDocument wordDoc) private void addActionInfo() { - List actionNodesList = flow.actions.ActionNodes.OrderBy(o => o.Name).ToList(); + List actionNodesList = content.actions.actionNodesList; Paragraph para = body.AppendChild(new Paragraph()); Run run = para.AppendChild(new Run()); run.AppendChild(new Text("Actions")); @@ -452,7 +369,7 @@ private void addActionInfo() { switchTable.Append(CreateRow(new Text(switchValue), new Paragraph(new Hyperlink(new Run(new Text(subaction.Name))) { - Anchor = subaction.Name, + Anchor = CreateMD5Hash(subaction.Name), DocLocation = "" }))); } @@ -466,13 +383,9 @@ private void addActionInfo() //adding a link to the subaction's section in the Word doc tc.Append(new Paragraph(new Hyperlink(new Run(new Text(subaction.Name))) { - Anchor = subaction.Name, + Anchor = CreateMD5Hash(subaction.Name), DocLocation = "" })); - if (action.switchRelationship.TryGetValue(subaction, out string switchValue)) - { - tc.Append(new Paragraph(new Run(new Text("Switch: " + switchValue)))); - } } } tr.Append(tc); @@ -494,7 +407,7 @@ private void addActionInfo() //adding a link to the elseaction's section in the Word doc tc.Append(new Paragraph(new Hyperlink(new Run(new Text(elseaction.Name))) { - Anchor = elseaction.Name, + Anchor = CreateMD5Hash(elseaction.Name), DocLocation = "" })); } @@ -519,7 +432,7 @@ private void addActionInfo() //adding a link to the next action's section in the Word doc tc.Append(new Paragraph(new Hyperlink(new Run(new Text(nextAction.Name))) { - Anchor = nextAction.Name, + Anchor = CreateMD5Hash(nextAction.Name), DocLocation = "" }, new Run(new Text(" [" + string.Join(", ", raConditions) + "]") { Space = SpaceProcessingModeValues.Preserve }))); } @@ -546,7 +459,7 @@ private void addFlowDetails(WordprocessingDocument wordDoc) //We add both the SVG and the PNG here. Modern clients (Office 2016 onwards?) can display the SVG. Others should use PNG as fallback ImagePart imagePart = wordDoc.MainDocumentPart.AddImagePart(ImagePartType.Png); int imageWidth, imageHeight; - using (FileStream stream = new FileStream(folderPath + "flow detailed.png", FileMode.Open)) + using (FileStream stream = new FileStream(content.folderPath + content.details.imageFileName + ".png", FileMode.Open)) { using (var image = Image.FromStream(stream, false, false)) { @@ -557,7 +470,7 @@ private void addFlowDetails(WordprocessingDocument wordDoc) imagePart.FeedData(stream); } ImagePart svgPart = wordDoc.MainDocumentPart.AddNewPart("image/svg+xml", "rId" + (new Random()).Next(100000, 999999)); - using (FileStream stream = new FileStream(folderPath + "flow detailed.svg", FileMode.Open)) + using (FileStream stream = new FileStream(content.folderPath + content.details.imageFileName + ".svg", FileMode.Open)) { svgPart.FeedData(stream); } diff --git a/PowerDocu.FlowDocumenter/GraphBuilder.cs b/PowerDocu.FlowDocumenter/GraphBuilder.cs index a4c55dd..80e1db2 100644 --- a/PowerDocu.FlowDocumenter/GraphBuilder.cs +++ b/PowerDocu.FlowDocumenter/GraphBuilder.cs @@ -76,7 +76,7 @@ private void buildGraph(bool showSubactions) private string generateImageFiles(RootGraph rootGraph, bool showSubactions) { //Generate image files - string filename = "flow" + (showSubactions ? " detailed" : ""); + string filename = "flow" + (showSubactions ? "-detailed" : ""); // can't save directly as PNG (limitation of the .Net Wrapper), saving as SVG is the only option rootGraph.ToSvgFile(folderPath + filename + ".svg"); //rootGraph.ToDotFile(folderPath + filename+".dot");