diff --git a/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/GetDataverseRibbonCmdlet.cs b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/GetDataverseRibbonCmdlet.cs new file mode 100644 index 000000000..62877a986 --- /dev/null +++ b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/GetDataverseRibbonCmdlet.cs @@ -0,0 +1,123 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using System; +using System.IO; +using System.IO.Compression; +using System.Management.Automation; +using System.Text; + +namespace Rnwood.Dataverse.Data.PowerShell.Commands +{ + /// + /// Retrieves ribbon customizations from a Dataverse environment. Ribbons can be retrieved for specific entities or for the application-wide ribbon. + /// + [Cmdlet(VerbsCommon.Get, "DataverseRibbon")] + [OutputType(typeof(PSObject))] + public class GetDataverseRibbonCmdlet : OrganizationServiceCmdlet + { + /// + /// Gets or sets the logical name of the entity/table for which to retrieve the ribbon. + /// If not specified, retrieves the application-wide ribbon. + /// + [Parameter(Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "Logical name of the entity/table for which to retrieve the ribbon. If not specified, retrieves the application-wide ribbon.")] + [ArgumentCompleter(typeof(TableNameArgumentCompleter))] + [Alias("EntityName", "TableName")] + public string Entity { get; set; } + + /// + /// Processes the cmdlet request. + /// + protected override void ProcessRecord() + { + base.ProcessRecord(); + + string ribbonDiffXml = null; + + if (!string.IsNullOrEmpty(Entity)) + { + // Retrieve entity-specific ribbon + WriteVerbose($"Retrieving ribbon for entity: {Entity}"); + ribbonDiffXml = RetrieveEntityRibbon(Entity); + WriteVerbose($"Retrieved entity ribbon, length: {ribbonDiffXml?.Length ?? 0}"); + } + else + { + // Retrieve application-wide ribbon + WriteVerbose("Retrieving application-wide ribbon"); + ribbonDiffXml = RetrieveApplicationRibbon(); + WriteVerbose($"Retrieved application ribbon, length: {ribbonDiffXml?.Length ?? 0}"); + } + + // Create output object + PSObject output = new PSObject(); + output.Properties.Add(new PSNoteProperty("Entity", Entity)); + output.Properties.Add(new PSNoteProperty("RibbonDiffXml", ribbonDiffXml)); + output.Properties.Add(new PSNoteProperty("IsApplicationRibbon", string.IsNullOrEmpty(Entity))); + + WriteObject(output); + } + + private string RetrieveEntityRibbon(string entityLogicalName) + { + try + { + RetrieveEntityRibbonRequest request = new RetrieveEntityRibbonRequest + { + EntityName = entityLogicalName, + RibbonLocationFilter = RibbonLocationFilters.All + }; + + RetrieveEntityRibbonResponse response = (RetrieveEntityRibbonResponse)Connection.Execute(request); + + if (response?.CompressedEntityXml == null) + { + WriteWarning($"Entity '{entityLogicalName}' not found or has no ribbon customizations."); + return null; + } + + // Decompress the XML + return DecompressXml(response.CompressedEntityXml); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to retrieve ribbon for entity '{entityLogicalName}': {ex.Message}", ex); + } + } + + private string RetrieveApplicationRibbon() + { + try + { + RetrieveApplicationRibbonRequest request = new RetrieveApplicationRibbonRequest(); + RetrieveApplicationRibbonResponse response = (RetrieveApplicationRibbonResponse)Connection.Execute(request); + + if (response?.CompressedApplicationRibbonXml == null) + { + WriteWarning("No application ribbon found."); + return null; + } + + // Decompress the XML + return DecompressXml(response.CompressedApplicationRibbonXml); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to retrieve application ribbon: {ex.Message}", ex); + } + } + + private string DecompressXml(byte[] compressedData) + { + if (compressedData == null || compressedData.Length == 0) + return null; + + using (MemoryStream memoryStream = new MemoryStream(compressedData)) + using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) + using (StreamReader reader = new StreamReader(gzipStream, Encoding.UTF8)) + { + return reader.ReadToEnd(); + } + } + } +} diff --git a/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/GetDataverseRibbonCommandDefinitionCmdlet.cs b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/GetDataverseRibbonCommandDefinitionCmdlet.cs new file mode 100644 index 000000000..a0d472e1a --- /dev/null +++ b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/GetDataverseRibbonCommandDefinitionCmdlet.cs @@ -0,0 +1,151 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Xrm.Sdk; +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Xml.Linq; + +namespace Rnwood.Dataverse.Data.PowerShell.Commands +{ + /// + /// Retrieves command definitions from a Dataverse ribbon (entity-specific or application-wide). + /// + [Cmdlet(VerbsCommon.Get, "DataverseRibbonCommandDefinition")] + [OutputType(typeof(PSObject))] + public class GetDataverseRibbonCommandDefinitionCmdlet : OrganizationServiceCmdlet + { + /// + /// Gets or sets the logical name of the entity/table for which to retrieve ribbon command definitions. + /// If not specified, retrieves from the application-wide ribbon. + /// + [Parameter(Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "Logical name of the entity/table. If not specified, retrieves from application-wide ribbon.")] + [ArgumentCompleter(typeof(TableNameArgumentCompleter))] + [Alias("EntityName", "TableName")] + public string Entity { get; set; } + + /// + /// Gets or sets the ID of a specific command definition to retrieve. + /// + [Parameter(HelpMessage = "ID of a specific command definition to retrieve")] + public string CommandId { get; set; } + + /// + /// Processes the cmdlet request. + /// + protected override void ProcessRecord() + { + base.ProcessRecord(); + + // Retrieve the ribbon + string ribbonDiffXml = RetrieveRibbon(); + + if (string.IsNullOrEmpty(ribbonDiffXml)) + { + WriteVerbose("No ribbon customizations found"); + return; + } + + // Parse the XML + XDocument doc = XDocument.Parse(ribbonDiffXml); + XNamespace ns = doc.Root.Name.Namespace; + + var commandDefinitionsElement = doc.Root.Element(ns + "CommandDefinitions"); + if (commandDefinitionsElement == null) + { + WriteVerbose("No CommandDefinitions element found in ribbon"); + return; + } + + var commandDefinitions = commandDefinitionsElement.Elements(ns + "CommandDefinition"); + + if (!string.IsNullOrEmpty(CommandId)) + { + commandDefinitions = commandDefinitions.Where(cd => cd.Attribute("Id")?.Value == CommandId); + } + + foreach (var commandDef in commandDefinitions) + { + PSObject output = new PSObject(); + output.Properties.Add(new PSNoteProperty("Entity", Entity)); + output.Properties.Add(new PSNoteProperty("Id", commandDef.Attribute("Id")?.Value)); + + // Get EnableRules + var enableRulesElement = commandDef.Element(ns + "EnableRules"); + if (enableRulesElement != null) + { + var enableRules = enableRulesElement.Elements(ns + "EnableRule") + .Select(er => er.Attribute("Id")?.Value) + .Where(id => !string.IsNullOrEmpty(id)) + .ToArray(); + output.Properties.Add(new PSNoteProperty("EnableRules", enableRules)); + } + + // Get DisplayRules + var displayRulesElement = commandDef.Element(ns + "DisplayRules"); + if (displayRulesElement != null) + { + var displayRules = displayRulesElement.Elements(ns + "DisplayRule") + .Select(dr => dr.Attribute("Id")?.Value) + .Where(id => !string.IsNullOrEmpty(id)) + .ToArray(); + output.Properties.Add(new PSNoteProperty("DisplayRules", displayRules)); + } + + // Get Actions + var actionsElement = commandDef.Element(ns + "Actions"); + if (actionsElement != null) + { + var actions = actionsElement.Elements() + .Select(a => new { Type = a.Name.LocalName, Library = a.Attribute("Library")?.Value, FunctionName = a.Attribute("FunctionName")?.Value }) + .ToArray(); + output.Properties.Add(new PSNoteProperty("Actions", actions)); + } + + output.Properties.Add(new PSNoteProperty("Xml", commandDef.ToString())); + + WriteObject(output); + } + } + + private string RetrieveRibbon() + { + if (!string.IsNullOrEmpty(Entity)) + { + // Retrieve entity-specific ribbon + WriteVerbose($"Retrieving ribbon for entity: {Entity}"); + RetrieveEntityRibbonRequest request = new RetrieveEntityRibbonRequest + { + EntityName = Entity, + RibbonLocationFilter = RibbonLocationFilters.All + }; + + RetrieveEntityRibbonResponse response = (RetrieveEntityRibbonResponse)Connection.Execute(request); + return DecompressXml(response.CompressedEntityXml); + } + else + { + // Retrieve application-wide ribbon + WriteVerbose("Retrieving application-wide ribbon"); + RetrieveApplicationRibbonRequest request = new RetrieveApplicationRibbonRequest(); + RetrieveApplicationRibbonResponse response = (RetrieveApplicationRibbonResponse)Connection.Execute(request); + return DecompressXml(response.CompressedApplicationRibbonXml); + } + } + + private string DecompressXml(byte[] compressedData) + { + if (compressedData == null || compressedData.Length == 0) + return null; + + using (MemoryStream memoryStream = new MemoryStream(compressedData)) + using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) + using (StreamReader reader = new StreamReader(gzipStream, Encoding.UTF8)) + { + return reader.ReadToEnd(); + } + } + } +} diff --git a/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/GetDataverseRibbonCustomActionCmdlet.cs b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/GetDataverseRibbonCustomActionCmdlet.cs new file mode 100644 index 000000000..790a511da --- /dev/null +++ b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/GetDataverseRibbonCustomActionCmdlet.cs @@ -0,0 +1,137 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Xrm.Sdk; +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Xml.Linq; + +namespace Rnwood.Dataverse.Data.PowerShell.Commands +{ + /// + /// Retrieves custom actions from a Dataverse ribbon (entity-specific or application-wide). + /// + [Cmdlet(VerbsCommon.Get, "DataverseRibbonCustomAction")] + [OutputType(typeof(PSObject))] + public class GetDataverseRibbonCustomActionCmdlet : OrganizationServiceCmdlet + { + /// + /// Gets or sets the logical name of the entity/table for which to retrieve ribbon custom actions. + /// If not specified, retrieves from the application-wide ribbon. + /// + [Parameter(Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "Logical name of the entity/table. If not specified, retrieves from application-wide ribbon.")] + [ArgumentCompleter(typeof(TableNameArgumentCompleter))] + [Alias("EntityName", "TableName")] + public string Entity { get; set; } + + /// + /// Gets or sets the ID of a specific custom action to retrieve. + /// + [Parameter(HelpMessage = "ID of a specific custom action to retrieve")] + public string CustomActionId { get; set; } + + /// + /// Processes the cmdlet request. + /// + protected override void ProcessRecord() + { + base.ProcessRecord(); + + // Retrieve the ribbon + string ribbonDiffXml = RetrieveRibbon(); + + if (string.IsNullOrEmpty(ribbonDiffXml)) + { + WriteVerbose("No ribbon customizations found"); + return; + } + + // Parse the XML + XDocument doc = XDocument.Parse(ribbonDiffXml); + XNamespace ns = doc.Root.Name.Namespace; + + var customActionsElement = doc.Root.Element(ns + "CustomActions"); + if (customActionsElement == null) + { + WriteVerbose("No CustomActions element found in ribbon"); + return; + } + + var customActions = customActionsElement.Elements(ns + "CustomAction"); + + if (!string.IsNullOrEmpty(CustomActionId)) + { + customActions = customActions.Where(ca => ca.Attribute("Id")?.Value == CustomActionId); + } + + foreach (var customAction in customActions) + { + PSObject output = new PSObject(); + output.Properties.Add(new PSNoteProperty("Entity", Entity)); + output.Properties.Add(new PSNoteProperty("Id", customAction.Attribute("Id")?.Value)); + output.Properties.Add(new PSNoteProperty("Location", customAction.Attribute("Location")?.Value)); + output.Properties.Add(new PSNoteProperty("Sequence", customAction.Attribute("Sequence")?.Value)); + output.Properties.Add(new PSNoteProperty("Title", customAction.Attribute("Title")?.Value)); + + // Get CommandUIDefinition + var commandUIDef = customAction.Element(ns + "CommandUIDefinition"); + if (commandUIDef != null) + { + // Get the first child element (Button, CheckBox, etc.) + var control = commandUIDef.Elements().FirstOrDefault(); + if (control != null) + { + output.Properties.Add(new PSNoteProperty("ControlType", control.Name.LocalName)); + output.Properties.Add(new PSNoteProperty("ControlId", control.Attribute("Id")?.Value)); + output.Properties.Add(new PSNoteProperty("Command", control.Attribute("Command")?.Value)); + output.Properties.Add(new PSNoteProperty("LabelText", control.Attribute("LabelText")?.Value)); + } + } + + output.Properties.Add(new PSNoteProperty("Xml", customAction.ToString())); + + WriteObject(output); + } + } + + private string RetrieveRibbon() + { + if (!string.IsNullOrEmpty(Entity)) + { + // Retrieve entity-specific ribbon + WriteVerbose($"Retrieving ribbon for entity: {Entity}"); + RetrieveEntityRibbonRequest request = new RetrieveEntityRibbonRequest + { + EntityName = Entity, + RibbonLocationFilter = RibbonLocationFilters.All + }; + + RetrieveEntityRibbonResponse response = (RetrieveEntityRibbonResponse)Connection.Execute(request); + return DecompressXml(response.CompressedEntityXml); + } + else + { + // Retrieve application-wide ribbon + WriteVerbose("Retrieving application-wide ribbon"); + RetrieveApplicationRibbonRequest request = new RetrieveApplicationRibbonRequest(); + RetrieveApplicationRibbonResponse response = (RetrieveApplicationRibbonResponse)Connection.Execute(request); + return DecompressXml(response.CompressedApplicationRibbonXml); + } + } + + private string DecompressXml(byte[] compressedData) + { + if (compressedData == null || compressedData.Length == 0) + return null; + + using (MemoryStream memoryStream = new MemoryStream(compressedData)) + using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) + using (StreamReader reader = new StreamReader(gzipStream, Encoding.UTF8)) + { + return reader.ReadToEnd(); + } + } + } +} diff --git a/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/GetDataverseRibbonRuleDefinitionCmdlet.cs b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/GetDataverseRibbonRuleDefinitionCmdlet.cs new file mode 100644 index 000000000..37722ad6d --- /dev/null +++ b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/GetDataverseRibbonRuleDefinitionCmdlet.cs @@ -0,0 +1,151 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Xrm.Sdk; +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Xml.Linq; + +namespace Rnwood.Dataverse.Data.PowerShell.Commands +{ + /// + /// Retrieves rule definitions from a Dataverse ribbon (entity-specific or application-wide). + /// + [Cmdlet(VerbsCommon.Get, "DataverseRibbonRuleDefinition")] + [OutputType(typeof(PSObject))] + public class GetDataverseRibbonRuleDefinitionCmdlet : OrganizationServiceCmdlet + { + /// + /// Gets or sets the logical name of the entity/table for which to retrieve ribbon rule definitions. + /// If not specified, retrieves from the application-wide ribbon. + /// + [Parameter(Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "Logical name of the entity/table. If not specified, retrieves from application-wide ribbon.")] + [ArgumentCompleter(typeof(TableNameArgumentCompleter))] + [Alias("EntityName", "TableName")] + public string Entity { get; set; } + + /// + /// Gets or sets the ID of a specific rule definition to retrieve. + /// + [Parameter(HelpMessage = "ID of a specific rule definition to retrieve")] + public string RuleId { get; set; } + + /// + /// Gets or sets the type of rules to retrieve (EnableRule or DisplayRule). + /// + [Parameter(HelpMessage = "Type of rules to retrieve (EnableRule or DisplayRule)")] + [ValidateSet("EnableRule", "DisplayRule")] + public string RuleType { get; set; } + + /// + /// Processes the cmdlet request. + /// + protected override void ProcessRecord() + { + base.ProcessRecord(); + + string ribbonDiffXml = RetrieveRibbon(); + + if (string.IsNullOrEmpty(ribbonDiffXml)) + { + WriteVerbose("No ribbon customizations found"); + return; + } + + XDocument doc = XDocument.Parse(ribbonDiffXml); + XNamespace ns = doc.Root.Name.Namespace; + + var ruleDefinitionsElement = doc.Root.Element(ns + "RuleDefinitions"); + if (ruleDefinitionsElement == null) + { + WriteVerbose("No RuleDefinitions element found in ribbon"); + return; + } + + // Process EnableRules if no type specified or EnableRule requested + if (string.IsNullOrEmpty(RuleType) || RuleType == "EnableRule") + { + var enableRulesElement = ruleDefinitionsElement.Element(ns + "EnableRules"); + if (enableRulesElement != null) + { + ProcessRules(enableRulesElement, ns + "EnableRule", "EnableRule"); + } + } + + // Process DisplayRules if no type specified or DisplayRule requested + if (string.IsNullOrEmpty(RuleType) || RuleType == "DisplayRule") + { + var displayRulesElement = ruleDefinitionsElement.Element(ns + "DisplayRules"); + if (displayRulesElement != null) + { + ProcessRules(displayRulesElement, ns + "DisplayRule", "DisplayRule"); + } + } + } + + private void ProcessRules(XElement rulesContainer, XName ruleName, string ruleType) + { + var rules = rulesContainer.Elements(ruleName); + + if (!string.IsNullOrEmpty(RuleId)) + { + rules = rules.Where(r => r.Attribute("Id")?.Value == RuleId); + } + + foreach (var rule in rules) + { + PSObject output = new PSObject(); + output.Properties.Add(new PSNoteProperty("Entity", Entity)); + output.Properties.Add(new PSNoteProperty("RuleType", ruleType)); + output.Properties.Add(new PSNoteProperty("Id", rule.Attribute("Id")?.Value)); + + // Get all child elements (the actual rule conditions) + var conditions = rule.Elements() + .Select(e => new { Type = e.Name.LocalName, Attributes = e.Attributes().ToDictionary(a => a.Name.LocalName, a => a.Value) }) + .ToArray(); + output.Properties.Add(new PSNoteProperty("Conditions", conditions)); + + output.Properties.Add(new PSNoteProperty("Xml", rule.ToString())); + + WriteObject(output); + } + } + + private string RetrieveRibbon() + { + if (!string.IsNullOrEmpty(Entity)) + { + WriteVerbose($"Retrieving ribbon for entity: {Entity}"); + RetrieveEntityRibbonRequest request = new RetrieveEntityRibbonRequest + { + EntityName = Entity, + RibbonLocationFilter = RibbonLocationFilters.All + }; + RetrieveEntityRibbonResponse response = (RetrieveEntityRibbonResponse)Connection.Execute(request); + return DecompressXml(response.CompressedEntityXml); + } + else + { + WriteVerbose("Retrieving application-wide ribbon"); + RetrieveApplicationRibbonRequest request = new RetrieveApplicationRibbonRequest(); + RetrieveApplicationRibbonResponse response = (RetrieveApplicationRibbonResponse)Connection.Execute(request); + return DecompressXml(response.CompressedApplicationRibbonXml); + } + } + + private string DecompressXml(byte[] compressedData) + { + if (compressedData == null || compressedData.Length == 0) + return null; + + using (MemoryStream memoryStream = new MemoryStream(compressedData)) + using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) + using (StreamReader reader = new StreamReader(gzipStream, Encoding.UTF8)) + { + return reader.ReadToEnd(); + } + } + } +} diff --git a/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/RemoveDataverseRibbonCommandDefinitionCmdlet.cs b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/RemoveDataverseRibbonCommandDefinitionCmdlet.cs new file mode 100644 index 000000000..674aa2296 --- /dev/null +++ b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/RemoveDataverseRibbonCommandDefinitionCmdlet.cs @@ -0,0 +1,215 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Management.Automation; +using System.Security; +using System.Text; +using System.Xml.Linq; + +namespace Rnwood.Dataverse.Data.PowerShell.Commands +{ + /// + /// Removes a command definition from a Dataverse ribbon (entity-specific or application-wide). + /// + [Cmdlet(VerbsCommon.Remove, "DataverseRibbonCommandDefinition", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RemoveDataverseRibbonCommandDefinitionCmdlet : OrganizationServiceCmdlet + { + /// + /// Gets or sets the logical name of the entity/table for which to remove the ribbon command definition. + /// If not specified, removes from the application-wide ribbon. + /// + [Parameter(Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "Logical name of the entity/table. If not specified, removes from application-wide ribbon.")] + [ArgumentCompleter(typeof(TableNameArgumentCompleter))] + [Alias("EntityName", "TableName")] + public string Entity { get; set; } + + /// + /// Gets or sets the ID of the command definition to remove. + /// + [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "ID of the command definition to remove")] + public string Id { get; set; } + + /// + /// Gets or sets whether to publish the customizations after removing the command definition. + /// + [Parameter(HelpMessage = "Publish the customizations after removing the command definition")] + public SwitchParameter Publish { get; set; } + + /// + /// Processes the cmdlet request. + /// + protected override void ProcessRecord() + { + base.ProcessRecord(); + + string target = string.IsNullOrEmpty(Entity) + ? $"command definition '{Id}' from application-wide ribbon" + : $"command definition '{Id}' from ribbon for entity '{Entity}'"; + + if (!ShouldProcess(target, "Remove")) + { + return; + } + + WriteVerbose($"Removing command definition '{Id}' from ribbon"); + RemoveCommandDefinition(); + WriteVerbose("Successfully removed command definition"); + + if (Publish.IsPresent) + { + WriteVerbose("Publishing customizations"); + PublishCustomizations(); + WriteVerbose("Customizations published successfully"); + } + } + + private void RemoveCommandDefinition() + { + try + { + string ribbonDiffXml = RetrieveRibbon(); + + if (string.IsNullOrEmpty(ribbonDiffXml)) + { + WriteWarning("No ribbon customizations found"); + return; + } + + XDocument doc = XDocument.Parse(ribbonDiffXml); + XNamespace ns = doc.Root.Name.Namespace; + + var commandDefinitionsElement = doc.Root.Element(ns + "CommandDefinitions"); + if (commandDefinitionsElement == null) + { + WriteWarning("No CommandDefinitions element found in ribbon"); + return; + } + + var commandDefToRemove = commandDefinitionsElement.Elements(ns + "CommandDefinition") + .FirstOrDefault(cd => cd.Attribute("Id")?.Value == Id); + + if (commandDefToRemove == null) + { + WriteWarning($"Command definition '{Id}' not found in ribbon"); + return; + } + + commandDefToRemove.Remove(); + WriteVerbose($"Removed command definition '{Id}' from ribbon XML"); + + SaveRibbon(doc.ToString()); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to remove command definition: {ex.Message}", ex); + } + } + + private string RetrieveRibbon() + { + if (!string.IsNullOrEmpty(Entity)) + { + RetrieveEntityRibbonRequest request = new RetrieveEntityRibbonRequest + { + EntityName = Entity, + RibbonLocationFilter = RibbonLocationFilters.All + }; + RetrieveEntityRibbonResponse response = (RetrieveEntityRibbonResponse)Connection.Execute(request); + return DecompressXml(response.CompressedEntityXml); + } + else + { + RetrieveApplicationRibbonRequest request = new RetrieveApplicationRibbonRequest(); + RetrieveApplicationRibbonResponse response = (RetrieveApplicationRibbonResponse)Connection.Execute(request); + return DecompressXml(response.CompressedApplicationRibbonXml); + } + } + + private void SaveRibbon(string ribbonDiffXml) + { + WriteWarning("Setting ribbon customizations requires solution import/export workflow for production scenarios."); + + if (!string.IsNullOrEmpty(Entity)) + { + QueryExpression query = new QueryExpression("ribbondiff") + { + ColumnSet = new ColumnSet("ribbondiffid"), + Criteria = new FilterExpression + { + Conditions = { new ConditionExpression("entity", ConditionOperator.Equal, Entity) } + }, + TopCount = 1 + }; + + EntityCollection results = Connection.RetrieveMultiple(query); + if (results.Entities.Count > 0) + { + Entity ribbonDiffEntity = results.Entities[0]; + ribbonDiffEntity["ribbondiffxml"] = ribbonDiffXml; + Connection.Update(ribbonDiffEntity); + } + else + { + WriteWarning("No existing ribbon diff found for entity"); + } + } + else + { + QueryExpression query = new QueryExpression("ribbondiff") + { + ColumnSet = new ColumnSet("ribbondiffid"), + Criteria = new FilterExpression + { + Conditions = { new ConditionExpression("entity", ConditionOperator.Null) } + }, + TopCount = 1 + }; + + EntityCollection results = Connection.RetrieveMultiple(query); + if (results.Entities.Count > 0) + { + Entity ribbonDiffEntity = results.Entities[0]; + ribbonDiffEntity["ribbondiffxml"] = ribbonDiffXml; + Connection.Update(ribbonDiffEntity); + } + else + { + WriteWarning("No existing ribbon diff found for application"); + } + } + } + + private void PublishCustomizations() + { + if (!string.IsNullOrEmpty(Entity)) + { + string parameterXml = $"{SecurityElement.Escape(Entity)}"; + PublishXmlRequest publishRequest = new PublishXmlRequest { ParameterXml = parameterXml }; + Connection.Execute(publishRequest); + } + else + { + PublishAllXmlRequest publishRequest = new PublishAllXmlRequest(); + Connection.Execute(publishRequest); + } + } + + private string DecompressXml(byte[] compressedData) + { + if (compressedData == null || compressedData.Length == 0) + return null; + + using (MemoryStream memoryStream = new MemoryStream(compressedData)) + using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) + using (StreamReader reader = new StreamReader(gzipStream, Encoding.UTF8)) + { + return reader.ReadToEnd(); + } + } + } +} diff --git a/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/RemoveDataverseRibbonCustomActionCmdlet.cs b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/RemoveDataverseRibbonCustomActionCmdlet.cs new file mode 100644 index 000000000..31260ca14 --- /dev/null +++ b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/RemoveDataverseRibbonCustomActionCmdlet.cs @@ -0,0 +1,234 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Management.Automation; +using System.Security; +using System.Text; +using System.Xml.Linq; + +namespace Rnwood.Dataverse.Data.PowerShell.Commands +{ + /// + /// Removes a custom action from a Dataverse ribbon (entity-specific or application-wide). + /// + [Cmdlet(VerbsCommon.Remove, "DataverseRibbonCustomAction", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class RemoveDataverseRibbonCustomActionCmdlet : OrganizationServiceCmdlet + { + /// + /// Gets or sets the logical name of the entity/table for which to remove the ribbon custom action. + /// If not specified, removes from the application-wide ribbon. + /// + [Parameter(Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "Logical name of the entity/table. If not specified, removes from application-wide ribbon.")] + [ArgumentCompleter(typeof(TableNameArgumentCompleter))] + [Alias("EntityName", "TableName")] + public string Entity { get; set; } + + /// + /// Gets or sets the ID of the custom action to remove. + /// + [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "ID of the custom action to remove")] + public string Id { get; set; } + + /// + /// Gets or sets whether to publish the customizations after removing the custom action. + /// + [Parameter(HelpMessage = "Publish the customizations after removing the custom action")] + public SwitchParameter Publish { get; set; } + + /// + /// Processes the cmdlet request. + /// + protected override void ProcessRecord() + { + base.ProcessRecord(); + + string target = string.IsNullOrEmpty(Entity) + ? $"custom action '{Id}' from application-wide ribbon" + : $"custom action '{Id}' from ribbon for entity '{Entity}'"; + + if (!ShouldProcess(target, "Remove")) + { + return; + } + + WriteVerbose($"Removing custom action '{Id}' from ribbon"); + RemoveCustomAction(); + WriteVerbose("Successfully removed custom action"); + + if (Publish.IsPresent) + { + WriteVerbose("Publishing customizations"); + PublishCustomizations(); + WriteVerbose("Customizations published successfully"); + } + } + + private void RemoveCustomAction() + { + try + { + // Retrieve current ribbon + string ribbonDiffXml = RetrieveRibbon(); + + if (string.IsNullOrEmpty(ribbonDiffXml)) + { + WriteWarning("No ribbon customizations found"); + return; + } + + XDocument doc = XDocument.Parse(ribbonDiffXml); + XNamespace ns = doc.Root.Name.Namespace; + + var customActionsElement = doc.Root.Element(ns + "CustomActions"); + if (customActionsElement == null) + { + WriteWarning("No CustomActions element found in ribbon"); + return; + } + + // Find and remove the custom action + var customActionToRemove = customActionsElement.Elements(ns + "CustomAction") + .FirstOrDefault(ca => ca.Attribute("Id")?.Value == Id); + + if (customActionToRemove == null) + { + WriteWarning($"Custom action '{Id}' not found in ribbon"); + return; + } + + customActionToRemove.Remove(); + WriteVerbose($"Removed custom action '{Id}' from ribbon XML"); + + // Save the updated ribbon + SaveRibbon(doc.ToString()); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to remove custom action: {ex.Message}", ex); + } + } + + private string RetrieveRibbon() + { + if (!string.IsNullOrEmpty(Entity)) + { + // Retrieve entity-specific ribbon + RetrieveEntityRibbonRequest request = new RetrieveEntityRibbonRequest + { + EntityName = Entity, + RibbonLocationFilter = RibbonLocationFilters.All + }; + + RetrieveEntityRibbonResponse response = (RetrieveEntityRibbonResponse)Connection.Execute(request); + return DecompressXml(response.CompressedEntityXml); + } + else + { + // Retrieve application-wide ribbon + RetrieveApplicationRibbonRequest request = new RetrieveApplicationRibbonRequest(); + RetrieveApplicationRibbonResponse response = (RetrieveApplicationRibbonResponse)Connection.Execute(request); + return DecompressXml(response.CompressedApplicationRibbonXml); + } + } + + private void SaveRibbon(string ribbonDiffXml) + { + WriteWarning("Setting ribbon customizations requires solution import/export workflow for production scenarios."); + + if (!string.IsNullOrEmpty(Entity)) + { + // Save entity ribbon + QueryExpression query = new QueryExpression("ribbondiff") + { + ColumnSet = new ColumnSet("ribbondiffid"), + Criteria = new FilterExpression + { + Conditions = + { + new ConditionExpression("entity", ConditionOperator.Equal, Entity) + } + }, + TopCount = 1 + }; + + EntityCollection results = Connection.RetrieveMultiple(query); + + if (results.Entities.Count > 0) + { + Entity ribbonDiffEntity = results.Entities[0]; + ribbonDiffEntity["ribbondiffxml"] = ribbonDiffXml; + Connection.Update(ribbonDiffEntity); + } + else + { + WriteWarning("No existing ribbon diff found for entity"); + } + } + else + { + // Save application ribbon + QueryExpression query = new QueryExpression("ribbondiff") + { + ColumnSet = new ColumnSet("ribbondiffid"), + Criteria = new FilterExpression + { + Conditions = + { + new ConditionExpression("entity", ConditionOperator.Null) + } + }, + TopCount = 1 + }; + + EntityCollection results = Connection.RetrieveMultiple(query); + + if (results.Entities.Count > 0) + { + Entity ribbonDiffEntity = results.Entities[0]; + ribbonDiffEntity["ribbondiffxml"] = ribbonDiffXml; + Connection.Update(ribbonDiffEntity); + } + else + { + WriteWarning("No existing ribbon diff found for application"); + } + } + } + + private void PublishCustomizations() + { + if (!string.IsNullOrEmpty(Entity)) + { + string parameterXml = $"{SecurityElement.Escape(Entity)}"; + PublishXmlRequest publishRequest = new PublishXmlRequest + { + ParameterXml = parameterXml + }; + Connection.Execute(publishRequest); + } + else + { + PublishAllXmlRequest publishRequest = new PublishAllXmlRequest(); + Connection.Execute(publishRequest); + } + } + + private string DecompressXml(byte[] compressedData) + { + if (compressedData == null || compressedData.Length == 0) + return null; + + using (MemoryStream memoryStream = new MemoryStream(compressedData)) + using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) + using (StreamReader reader = new StreamReader(gzipStream, Encoding.UTF8)) + { + return reader.ReadToEnd(); + } + } + } +} diff --git a/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/SetDataverseRibbonCmdlet.cs b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/SetDataverseRibbonCmdlet.cs new file mode 100644 index 000000000..a43286b3e --- /dev/null +++ b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/SetDataverseRibbonCmdlet.cs @@ -0,0 +1,229 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; +using System; +using System.IO; +using System.IO.Compression; +using System.Management.Automation; +using System.Security; +using System.Text; + +namespace Rnwood.Dataverse.Data.PowerShell.Commands +{ + /// + /// Creates or updates ribbon customizations in a Dataverse environment. Ribbons can be set for specific entities or for the application-wide ribbon. + /// Note: Ribbon customizations typically require exporting/modifying/importing solutions. This cmdlet provides a simplified interface but may have limitations. + /// + [Cmdlet(VerbsCommon.Set, "DataverseRibbon", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class SetDataverseRibbonCmdlet : OrganizationServiceCmdlet + { + /// + /// Gets or sets the logical name of the entity/table for which to set the ribbon. + /// If not specified, sets the application-wide ribbon. + /// + [Parameter(Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "Logical name of the entity/table for which to set the ribbon. If not specified, sets the application-wide ribbon.")] + [ArgumentCompleter(typeof(TableNameArgumentCompleter))] + [Alias("EntityName", "TableName")] + public string Entity { get; set; } + + /// + /// Gets or sets the RibbonDiffXml content. This must be valid XML conforming to the Dataverse Ribbon schema. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "RibbonDiffXml content. Must be valid XML conforming to the Dataverse Ribbon schema.")] + [Alias("RibbonXml", "Xml")] + public string RibbonDiffXml { get; set; } + + /// + /// Gets or sets whether to publish the customizations after setting the ribbon. + /// + [Parameter(HelpMessage = "Publish the customizations after setting the ribbon")] + public SwitchParameter Publish { get; set; } + + /// + /// Processes the cmdlet request. + /// + protected override void ProcessRecord() + { + base.ProcessRecord(); + + // Validate RibbonDiffXml is provided + if (string.IsNullOrWhiteSpace(RibbonDiffXml)) + { + throw new ArgumentException("RibbonDiffXml cannot be null or empty.", nameof(RibbonDiffXml)); + } + + if (!string.IsNullOrEmpty(Entity)) + { + // Set entity-specific ribbon + string target = $"ribbon for entity '{Entity}'"; + if (!ShouldProcess(target, "Update")) + { + return; + } + + WriteVerbose($"Setting ribbon for entity: {Entity}"); + SetEntityRibbon(Entity, RibbonDiffXml); + WriteVerbose($"Successfully set entity ribbon"); + + if (Publish.IsPresent) + { + WriteVerbose($"Publishing customizations for entity: {Entity}"); + PublishEntity(Entity); + WriteVerbose("Customizations published successfully"); + } + } + else + { + // Set application-wide ribbon + string target = "application-wide ribbon"; + if (!ShouldProcess(target, "Update")) + { + return; + } + + WriteVerbose("Setting application-wide ribbon"); + SetApplicationRibbon(RibbonDiffXml); + WriteVerbose("Successfully set application ribbon"); + + if (Publish.IsPresent) + { + WriteVerbose("Publishing application customizations"); + PublishAllXml(); + WriteVerbose("Customizations published successfully"); + } + } + } + + private void SetEntityRibbon(string entityLogicalName, string ribbonDiffXml) + { + try + { + // Create/update the ribboncommand solution component for entity + // We'll store the ribbon diff in the ribbondiff table + WriteWarning("Setting entity ribbons requires solution import/export workflow. " + + "Consider using solution export, modification, and import for production scenarios."); + + // Query for existing ribbon diff for this entity + QueryExpression query = new QueryExpression("ribbondiff") + { + ColumnSet = new ColumnSet("ribbondiffid", "entity"), + Criteria = new FilterExpression + { + Conditions = + { + new ConditionExpression("entity", ConditionOperator.Equal, entityLogicalName) + } + }, + TopCount = 1 + }; + + EntityCollection results = Connection.RetrieveMultiple(query); + + Entity ribbonDiffEntity; + if (results.Entities.Count > 0) + { + // Update existing ribbon diff + ribbonDiffEntity = results.Entities[0]; + ribbonDiffEntity["ribbondiffxml"] = ribbonDiffXml; + Connection.Update(ribbonDiffEntity); + WriteVerbose($"Updated existing ribbon diff for entity '{entityLogicalName}'"); + } + else + { + // Create new ribbon diff + ribbonDiffEntity = new Entity("ribbondiff"); + ribbonDiffEntity["entity"] = entityLogicalName; + ribbonDiffEntity["ribbondiffxml"] = ribbonDiffXml; + + Connection.Create(ribbonDiffEntity); + WriteVerbose($"Created new ribbon diff for entity '{entityLogicalName}'"); + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to set ribbon for entity '{entityLogicalName}': {ex.Message}", ex); + } + } + + private void SetApplicationRibbon(string ribbonDiffXml) + { + try + { + WriteWarning("Setting application ribbons requires solution import/export workflow. " + + "Consider using solution export, modification, and import for production scenarios."); + + // For application-level ribbons, entity field is null + QueryExpression query = new QueryExpression("ribbondiff") + { + ColumnSet = new ColumnSet("ribbondiffid", "entity"), + Criteria = new FilterExpression + { + Conditions = + { + new ConditionExpression("entity", ConditionOperator.Null) + } + }, + TopCount = 1 + }; + + EntityCollection results = Connection.RetrieveMultiple(query); + + Entity ribbonDiffEntity; + if (results.Entities.Count > 0) + { + // Update existing application ribbon diff + ribbonDiffEntity = results.Entities[0]; + ribbonDiffEntity["ribbondiffxml"] = ribbonDiffXml; + Connection.Update(ribbonDiffEntity); + WriteVerbose("Updated existing application ribbon diff"); + } + else + { + // Create new application ribbon diff + ribbonDiffEntity = new Entity("ribbondiff"); + ribbonDiffEntity["ribbondiffxml"] = ribbonDiffXml; + + Connection.Create(ribbonDiffEntity); + WriteVerbose("Created new application ribbon diff"); + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to set application ribbon: {ex.Message}", ex); + } + } + + private void PublishEntity(string entityLogicalName) + { + try + { + string parameterXml = $"{SecurityElement.Escape(entityLogicalName)}"; + + PublishXmlRequest publishRequest = new PublishXmlRequest + { + ParameterXml = parameterXml + }; + + Connection.Execute(publishRequest); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to publish customizations for entity '{entityLogicalName}': {ex.Message}", ex); + } + } + + private void PublishAllXml() + { + try + { + PublishAllXmlRequest publishRequest = new PublishAllXmlRequest(); + Connection.Execute(publishRequest); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to publish all customizations: {ex.Message}", ex); + } + } + } +} diff --git a/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/SetDataverseRibbonCommandDefinitionCmdlet.cs b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/SetDataverseRibbonCommandDefinitionCmdlet.cs new file mode 100644 index 000000000..d4bf5274b --- /dev/null +++ b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/SetDataverseRibbonCommandDefinitionCmdlet.cs @@ -0,0 +1,259 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Management.Automation; +using System.Security; +using System.Text; +using System.Xml.Linq; + +namespace Rnwood.Dataverse.Data.PowerShell.Commands +{ + /// + /// Creates or updates a command definition in a Dataverse ribbon (entity-specific or application-wide). + /// + [Cmdlet(VerbsCommon.Set, "DataverseRibbonCommandDefinition", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class SetDataverseRibbonCommandDefinitionCmdlet : OrganizationServiceCmdlet + { + /// + /// Gets or sets the logical name of the entity/table for which to set the ribbon command definition. + /// If not specified, sets in the application-wide ribbon. + /// + [Parameter(Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "Logical name of the entity/table. If not specified, sets in application-wide ribbon.")] + [ArgumentCompleter(typeof(TableNameArgumentCompleter))] + [Alias("EntityName", "TableName")] + public string Entity { get; set; } + + /// + /// Gets or sets the ID of the command definition. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "ID of the command definition")] + public string Id { get; set; } + + /// + /// Gets or sets the XML content for the command definition (EnableRules, DisplayRules, Actions elements). + /// + [Parameter(Mandatory = true, HelpMessage = "XML content for the command definition (e.g., .........)")] + public string CommandDefinitionXml { get; set; } + + /// + /// Gets or sets whether to publish the customizations after setting the command definition. + /// + [Parameter(HelpMessage = "Publish the customizations after setting the command definition")] + public SwitchParameter Publish { get; set; } + + /// + /// Processes the cmdlet request. + /// + protected override void ProcessRecord() + { + base.ProcessRecord(); + + string target = string.IsNullOrEmpty(Entity) + ? "command definition in application-wide ribbon" + : $"command definition in ribbon for entity '{Entity}'"; + + if (!ShouldProcess(target, "Update")) + { + return; + } + + WriteVerbose($"Setting command definition '{Id}' in ribbon"); + SetCommandDefinition(); + WriteVerbose("Successfully set command definition"); + + if (Publish.IsPresent) + { + WriteVerbose("Publishing customizations"); + PublishCustomizations(); + WriteVerbose("Customizations published successfully"); + } + } + + private void SetCommandDefinition() + { + try + { + // Retrieve current ribbon + string ribbonDiffXml = RetrieveRibbon(); + XDocument doc; + + if (string.IsNullOrEmpty(ribbonDiffXml)) + { + doc = new XDocument(new XElement("RibbonDiffXml")); + } + else + { + doc = XDocument.Parse(ribbonDiffXml); + } + + XNamespace ns = doc.Root.Name.Namespace; + + // Ensure CommandDefinitions element exists + var commandDefinitionsElement = doc.Root.Element(ns + "CommandDefinitions"); + if (commandDefinitionsElement == null) + { + commandDefinitionsElement = new XElement(ns + "CommandDefinitions"); + // Insert after CustomActions if it exists, otherwise as first element + var customActions = doc.Root.Element(ns + "CustomActions"); + if (customActions != null) + { + customActions.AddAfterSelf(commandDefinitionsElement); + } + else + { + doc.Root.AddFirst(commandDefinitionsElement); + } + } + + // Find existing command definition or create new + var existingCommand = commandDefinitionsElement.Elements(ns + "CommandDefinition") + .FirstOrDefault(cd => cd.Attribute("Id")?.Value == Id); + + XElement commandDefElement; + if (existingCommand != null) + { + commandDefElement = existingCommand; + commandDefElement.RemoveAll(); + WriteVerbose($"Updating existing command definition '{Id}'"); + } + else + { + commandDefElement = new XElement(ns + "CommandDefinition"); + commandDefinitionsElement.Add(commandDefElement); + WriteVerbose($"Creating new command definition '{Id}'"); + } + + // Set attributes + commandDefElement.SetAttributeValue("Id", Id); + + // Parse and add the provided XML content + XElement contentElement = XElement.Parse($"{CommandDefinitionXml}"); + foreach (var child in contentElement.Elements()) + { + commandDefElement.Add(child); + } + + // Save the updated ribbon + SaveRibbon(doc.ToString()); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to set command definition: {ex.Message}", ex); + } + } + + private string RetrieveRibbon() + { + if (!string.IsNullOrEmpty(Entity)) + { + RetrieveEntityRibbonRequest request = new RetrieveEntityRibbonRequest + { + EntityName = Entity, + RibbonLocationFilter = RibbonLocationFilters.All + }; + RetrieveEntityRibbonResponse response = (RetrieveEntityRibbonResponse)Connection.Execute(request); + return DecompressXml(response.CompressedEntityXml); + } + else + { + RetrieveApplicationRibbonRequest request = new RetrieveApplicationRibbonRequest(); + RetrieveApplicationRibbonResponse response = (RetrieveApplicationRibbonResponse)Connection.Execute(request); + return DecompressXml(response.CompressedApplicationRibbonXml); + } + } + + private void SaveRibbon(string ribbonDiffXml) + { + WriteWarning("Setting ribbon customizations requires solution import/export workflow for production scenarios."); + + if (!string.IsNullOrEmpty(Entity)) + { + QueryExpression query = new QueryExpression("ribbondiff") + { + ColumnSet = new ColumnSet("ribbondiffid"), + Criteria = new FilterExpression + { + Conditions = { new ConditionExpression("entity", ConditionOperator.Equal, Entity) } + }, + TopCount = 1 + }; + + EntityCollection results = Connection.RetrieveMultiple(query); + Entity ribbonDiffEntity; + if (results.Entities.Count > 0) + { + ribbonDiffEntity = results.Entities[0]; + ribbonDiffEntity["ribbondiffxml"] = ribbonDiffXml; + Connection.Update(ribbonDiffEntity); + } + else + { + ribbonDiffEntity = new Entity("ribbondiff"); + ribbonDiffEntity["entity"] = Entity; + ribbonDiffEntity["ribbondiffxml"] = ribbonDiffXml; + Connection.Create(ribbonDiffEntity); + } + } + else + { + QueryExpression query = new QueryExpression("ribbondiff") + { + ColumnSet = new ColumnSet("ribbondiffid"), + Criteria = new FilterExpression + { + Conditions = { new ConditionExpression("entity", ConditionOperator.Null) } + }, + TopCount = 1 + }; + + EntityCollection results = Connection.RetrieveMultiple(query); + Entity ribbonDiffEntity; + if (results.Entities.Count > 0) + { + ribbonDiffEntity = results.Entities[0]; + ribbonDiffEntity["ribbondiffxml"] = ribbonDiffXml; + Connection.Update(ribbonDiffEntity); + } + else + { + ribbonDiffEntity = new Entity("ribbondiff"); + ribbonDiffEntity["ribbondiffxml"] = ribbonDiffXml; + Connection.Create(ribbonDiffEntity); + } + } + } + + private void PublishCustomizations() + { + if (!string.IsNullOrEmpty(Entity)) + { + string parameterXml = $"{SecurityElement.Escape(Entity)}"; + PublishXmlRequest publishRequest = new PublishXmlRequest { ParameterXml = parameterXml }; + Connection.Execute(publishRequest); + } + else + { + PublishAllXmlRequest publishRequest = new PublishAllXmlRequest(); + Connection.Execute(publishRequest); + } + } + + private string DecompressXml(byte[] compressedData) + { + if (compressedData == null || compressedData.Length == 0) + return null; + + using (MemoryStream memoryStream = new MemoryStream(compressedData)) + using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) + using (StreamReader reader = new StreamReader(gzipStream, Encoding.UTF8)) + { + return reader.ReadToEnd(); + } + } + } +} diff --git a/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/SetDataverseRibbonCustomActionCmdlet.cs b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/SetDataverseRibbonCustomActionCmdlet.cs new file mode 100644 index 000000000..58a141a4f --- /dev/null +++ b/Rnwood.Dataverse.Data.PowerShell.Cmdlets/Commands/SetDataverseRibbonCustomActionCmdlet.cs @@ -0,0 +1,299 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Management.Automation; +using System.Security; +using System.Text; +using System.Xml.Linq; + +namespace Rnwood.Dataverse.Data.PowerShell.Commands +{ + /// + /// Creates or updates a custom action in a Dataverse ribbon (entity-specific or application-wide). + /// + [Cmdlet(VerbsCommon.Set, "DataverseRibbonCustomAction", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(void))] + public class SetDataverseRibbonCustomActionCmdlet : OrganizationServiceCmdlet + { + /// + /// Gets or sets the logical name of the entity/table for which to set the ribbon custom action. + /// If not specified, sets in the application-wide ribbon. + /// + [Parameter(Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "Logical name of the entity/table. If not specified, sets in application-wide ribbon.")] + [ArgumentCompleter(typeof(TableNameArgumentCompleter))] + [Alias("EntityName", "TableName")] + public string Entity { get; set; } + + /// + /// Gets or sets the ID of the custom action. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "ID of the custom action")] + public string Id { get; set; } + + /// + /// Gets or sets the location where the custom action should appear. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Location where the custom action should appear (e.g., Mscrm.HomepageGrid.contact.MainTab.Actions.Controls._children)")] + public string Location { get; set; } + + /// + /// Gets or sets the sequence number that determines the order of the custom action. + /// + [Parameter(HelpMessage = "Sequence number (default: 100)")] + public int Sequence { get; set; } = 100; + + /// + /// Gets or sets the title of the custom action. + /// + [Parameter(HelpMessage = "Title of the custom action")] + public string Title { get; set; } + + /// + /// Gets or sets the XML content for the CommandUIDefinition. + /// + [Parameter(Mandatory = true, HelpMessage = "XML content for the CommandUIDefinition element (e.g.,