Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ public sealed record EntityAttributeRecord(
bool IsPrimaryId,
bool IsPrimaryName,
int? MaxLength,
string? Description);
string? Description,
string? OptionSetName = null,
string? OptionValues = null);

/// <summary>
/// Relationship summary for an entity, returned by
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,29 @@ Task CreateGlobalOptionSetAsync(
CancellationToken ct);

/// <summary>
/// Inserts a new option value into a local or global option set.
/// Inserts a new option value into an option set.
/// For a local option set, provide <paramref name="entityName"/> and <paramref name="attributeName"/>.
/// For a global option set, provide <paramref name="globalOptionSetName"/>.
/// For a global option set, provide <paramref name="optionSetName"/>.
/// </summary>
Task InsertOptionAsync(
string? profileName,
string? entityName,
string? attributeName,
string? globalOptionSetName,
string? optionSetName,
string label,
int? value,
CancellationToken ct);

/// <summary>
/// Deletes an option value from a local or global option set.
/// Deletes an option value from an option set.
/// For a local option set, provide <paramref name="entityName"/> and <paramref name="attributeName"/>.
/// For a global option set, provide <paramref name="globalOptionSetName"/>.
/// For a global option set, provide <paramref name="optionSetName"/>.
/// </summary>
Task DeleteOptionAsync(
string? profileName,
string? entityName,
string? attributeName,
string? globalOptionSetName,
string? optionSetName,
int value,
CancellationToken ct);

Expand All @@ -71,9 +71,65 @@ Task DeleteGlobalOptionSetAsync(
Task<IReadOnlyList<GlobalOptionSetSummaryRecord>> ListGlobalOptionSetsAsync(
string? profileName,
CancellationToken ct);

/// <summary>
/// Describes a specific global option set — returns its options (value + label pairs).
/// </summary>
/// <param name="languageCode">Optional LCID for label language (e.g. 1033=English, 1029=Czech). Null = user's language.</param>
Task<GlobalOptionSetDetailRecord> DescribeGlobalOptionSetAsync(
string? profileName,
string optionSetName,
int? languageCode,
CancellationToken ct);
}

/// <summary>
/// Input DTO for a single option (label + optional value) used when creating option sets.
/// </summary>
public sealed record OptionMetadataInput(string Label, int Value);
public sealed record OptionMetadataInput(string Label, int Value)
{
/// <summary>
/// Parses a comma-separated options string into <see cref="OptionMetadataInput"/> items.
/// Supports "Label:Value" pairs or plain "Label" (auto-valued starting at 100000000).
/// </summary>
public static OptionMetadataInput[] ParseCsv(string csv)
{
var entries = csv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (entries.Length == 0)
return Array.Empty<OptionMetadataInput>();

var results = new OptionMetadataInput[entries.Length];
int autoValue = 100_000_000;

for (int i = 0; i < entries.Length; i++)
{
var parts = entries[i].Split(':', 2);
if (parts.Length == 2 && int.TryParse(parts[1].Trim(), out int v))
{
results[i] = new OptionMetadataInput(parts[0].Trim(), v);
}
else
{
results[i] = new OptionMetadataInput(parts[0].Trim(), autoValue++);
}
}

return results;
}
};

/// <summary>
/// Detail record for a global option set, including all option values and labels.
/// </summary>
public sealed record GlobalOptionSetDetailRecord(
string Name,
string? DisplayName,
string? Description,
string OptionSetType,
bool IsCustomOptionSet,
IReadOnlyList<OptionValueRecord> Options);

/// <summary>
/// A single option value within an option set.
/// </summary>
public sealed record OptionValueRecord(int Value, string? Label, string? Description);
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace TALXIS.CLI.Features.Environment.Data.Bulk;
[CliIdempotent]
[CliCommand(
Name = "create",
Description = "Creates multiple Dataverse records in a single batch request on the LIVE connected environment. Requires an active profile. Accepts JSON array via --data or --file."
Description = "Creates multiple Dataverse records in a single batch request on the LIVE connected environment. Requires an active profile. Accepts JSON array via --data or --file. Column types are auto-detected: option sets accept plain integers (e.g. 375970000), money fields accept decimals, lookups accept {Id,LogicalName} objects or a bare GUID string (single-target lookups)."
)]
public class EnvDataBulkCreateCliCommand : ProfiledCliCommand
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace TALXIS.CLI.Features.Environment.Data.Record;
[CliIdempotent]
[CliCommand(
Name = "create",
Description = "Creates a single Dataverse record in the LIVE connected environment from inline JSON or file. Requires an active profile. For LOCAL component scaffolding, use 'workspace component create' instead."
Description = "Creates a single Dataverse record in the LIVE connected environment from inline JSON or file. Requires an active profile. Column types are auto-detected: option sets accept plain integers (e.g. 375970000), money fields accept decimals, lookups accept {Id,LogicalName} objects or a bare GUID string (single-target lookups). For LOCAL component scaffolding, use 'workspace component create' instead."
)]
#pragma warning disable TXC003
public class EnvDataRecordCreateCliCommand : StagedCliCommand
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace TALXIS.CLI.Features.Environment.Entity;
[CliCommand(
Name = "entity",
Description = "Entity discovery and schema metadata for the live environment.",
Children = new[] { typeof(EntityListCliCommand), typeof(EntityDescribeCliCommand), typeof(EntityGetCliCommand), typeof(EntityUpdateCliCommand), typeof(EntityCreateCliCommand), typeof(EntityDeleteCliCommand), typeof(EntityAttributeCliCommand), typeof(EntityRelationshipCliCommand), typeof(EntityOptionSetCliCommand) }
Children = new[] { typeof(EntityListCliCommand), typeof(EntityDescribeCliCommand), typeof(EntityGetCliCommand), typeof(EntityUpdateCliCommand), typeof(EntityCreateCliCommand), typeof(EntityDeleteCliCommand), typeof(EntityAttributeCliCommand), typeof(EntityRelationshipCliCommand) }
)]
public class EntityCliCommand
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ private static void PrintAttributesTable(IReadOnlyList<EntityAttributeRecord> ro
int pkWidth = 2;
int nameWidth = 4;
int maxLenWidth = 10;
bool hasOptionSets = rows.Any(r => r.OptionSetName is not null);
int optSetWidth = hasOptionSets
? Math.Clamp(rows.Where(r => r.OptionSetName is not null).Max(r => r.OptionSetName!.Length), 9, 30)
: 0;

string header =
$"{"Logical Name".PadRight(logicalWidth)} | " +
Expand All @@ -61,6 +65,8 @@ private static void PrintAttributesTable(IReadOnlyList<EntityAttributeRecord> ro
$"{"PK".PadRight(pkWidth)} | " +
$"{"Name".PadRight(nameWidth)} | " +
$"{"Max Length".PadRight(maxLenWidth)}";
if (hasOptionSets)
header += $" | {"OptionSet".PadRight(optSetWidth)}";
OutputWriter.WriteLine(header);
OutputWriter.WriteLine(new string('-', header.Length));

Expand All @@ -74,14 +80,17 @@ private static void PrintAttributesTable(IReadOnlyList<EntityAttributeRecord> ro
string name = r.IsPrimaryName ? "*" : "";
string maxLen = r.MaxLength.HasValue ? r.MaxLength.Value.ToString() : "";

OutputWriter.WriteLine(
string line =
$"{logical.PadRight(logicalWidth)} | " +
$"{type.PadRight(typeWidth)} | " +
$"{display.PadRight(displayWidth)} | " +
$"{custom.PadRight(customWidth)} | " +
$"{pk.PadRight(pkWidth)} | " +
$"{name.PadRight(nameWidth)} | " +
$"{maxLen.PadRight(maxLenWidth)}");
$"{maxLen.PadRight(maxLenWidth)}";
if (hasOptionSets)
line += $" | {Truncate(r.OptionSetName ?? "", optSetWidth).PadRight(optSetWidth)}";
OutputWriter.WriteLine(line);
Comment thread
TomProkop marked this conversation as resolved.
}
}
#pragma warning restore TXC003
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace TALXIS.CLI.Features.Environment;
Name = "environment",
Alias = "env",
Description = "Manage the footprint of your project in a live target environment (packages, solutions, deployment history).",
Children = new[] { typeof(Package.PackageCliCommand), typeof(Solution.SolutionCliCommand), typeof(Deployment.DeploymentCliCommand), typeof(Data.EnvDataCliCommand), typeof(Entity.EntityCliCommand), typeof(Setting.SettingCliCommand), typeof(Changeset.ChangesetCliCommand), typeof(Component.ComponentCliCommand), typeof(Publisher.PublisherCliCommand) },
Children = new[] { typeof(Package.PackageCliCommand), typeof(Solution.SolutionCliCommand), typeof(Deployment.DeploymentCliCommand), typeof(Data.EnvDataCliCommand), typeof(Entity.EntityCliCommand), typeof(OptionSet.OptionSetCliCommand), typeof(Setting.SettingCliCommand), typeof(Changeset.ChangesetCliCommand), typeof(Component.ComponentCliCommand), typeof(Publisher.PublisherCliCommand) },
ShortFormAutoGenerate = CliNameAutoGenerate.None
)]
public class EnvironmentCliCommand
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,57 +6,55 @@
using TALXIS.CLI.Core.DependencyInjection;
using TALXIS.CLI.Logging;

namespace TALXIS.CLI.Features.Environment.Entity;
namespace TALXIS.CLI.Features.Environment.OptionSet;

/// <summary>
/// Adds an option value to a local or global option set.
/// Usage: <c>txc environment entity optionset option add --label &lt;text&gt; (--global-optionset &lt;name&gt; | --entity &lt;name&gt; --attribute &lt;name&gt;) [--value &lt;int&gt;]</c>
/// Adds an option value to a global or local option set.
/// Global: <c>txc environment optionset add-option --name &lt;schema-name&gt; --label &lt;text&gt;</c>
/// Local: <c>txc environment optionset add-option --entity &lt;name&gt; --attribute &lt;name&gt; --label &lt;text&gt;</c>
/// </summary>
[CliIdempotent]
[CliCommand(
Name = "add",
Description = "Add an option value to a local or global option set."
Description = "Add an option value to a global or local option set."
)]
#pragma warning disable TXC003
public class EntityOptionSetAddOptionCliCommand : StagedCliCommand
public class OptionSetAddOptionCliCommand : StagedCliCommand
{
protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(nameof(EntityOptionSetAddOptionCliCommand));
protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(nameof(OptionSetAddOptionCliCommand));

[CliOption(Name = "--entity", Description = "The logical name of the entity (for local option sets).", Required = false)]
[CliOption(Name = "--name", Description = "Schema name of the global option set.", Required = false)]
public string? Name { get; set; }

[CliOption(Name = "--entity", Description = "Entity logical name (for local option sets).", Required = false)]
public string? Entity { get; set; }

[CliOption(Name = "--attribute", Description = "The logical name of the attribute (for local option sets).", Required = false)]
[CliOption(Name = "--attribute", Description = "Attribute logical name (for local option sets).", Required = false)]
public string? Attribute { get; set; }

[CliOption(Name = "--global-optionset", Description = "The name of the global option set (mutually exclusive with --entity/--attribute).", Required = false)]
public string? GlobalOptionset { get; set; }

[CliOption(Name = "--label", Description = "The label for the new option.", Required = true)]
[CliOption(Name = "--label", Description = "Label for the new option.", Required = true)]
public string Label { get; set; } = null!;

[CliOption(Name = "--value", Description = "The integer value for the new option (auto-generated if not provided).", Required = false)]
[CliOption(Name = "--value", Description = "Integer value for the new option (auto-generated if omitted).", Required = false)]
public int? Value { get; set; }

protected override async Task<int> ExecuteAsync()
{
ValidateExecutionMode();

// Validate mutually exclusive options.
bool hasGlobal = !string.IsNullOrWhiteSpace(GlobalOptionset);
bool hasGlobal = !string.IsNullOrWhiteSpace(Name);
bool hasLocal = !string.IsNullOrWhiteSpace(Entity) || !string.IsNullOrWhiteSpace(Attribute);

if (hasGlobal && hasLocal)
{
Logger.LogError("Specify either --global-optionset or --entity/--attribute, not both.");
Logger.LogError("Specify either --name (global) or --entity + --attribute (local), not both.");
return ExitError;
}

if (!hasGlobal && !hasLocal)
{
Logger.LogError("Specify --global-optionset for a global option set, or --entity and --attribute for a local one.");
Logger.LogError("Specify --name for a global option set, or --entity and --attribute for a local one.");
return ExitError;
}

if (hasLocal && (string.IsNullOrWhiteSpace(Entity) || string.IsNullOrWhiteSpace(Attribute)))
{
Logger.LogError("Both --entity and --attribute are required for local option sets.");
Expand All @@ -65,20 +63,20 @@ protected override async Task<int> ExecuteAsync()

if (Stage)
{
string stageTarget = hasGlobal ? GlobalOptionset! : $"{Entity}.{Attribute}";
string stageTarget = hasGlobal ? Name! : $"{Entity}.{Attribute}";
var store = TxcServices.Get<IChangesetStore>();
store.Add(new StagedOperation
{
Category = "schema",
OperationType = "CREATE",
TargetType = "optionset",
TargetType = "optionset-option",
TargetDescription = stageTarget,
Details = $"add option: \"{Label}\"" + (Value.HasValue ? $" ({Value})" : ""),
Parameters = new Dictionary<string, object?>
{
["entity"] = Entity,
["attribute"] = Attribute,
["globalOptionset"] = GlobalOptionset,
["name"] = Name,
["label"] = Label,
["value"] = Value
}
Expand All @@ -89,10 +87,10 @@ protected override async Task<int> ExecuteAsync()

var service = TxcServices.Get<IDataverseOptionSetService>();
await service.InsertOptionAsync(
Profile, Entity, Attribute, GlobalOptionset, Label, Value, CancellationToken.None
Profile, Entity, Attribute, Name, Label, Value, CancellationToken.None
).ConfigureAwait(false);

string target = hasGlobal ? $"global option set '{GlobalOptionset}'" : $"attribute '{Attribute}' on entity '{Entity}'";
string target = hasGlobal ? $"global option set '{Name}'" : $"attribute '{Attribute}' on entity '{Entity}'";
OutputWriter.WriteLine($"Option '{Label}' added to {target}.");
return ExitSuccess;
}
Expand Down
Loading