Skip to content
Open
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
51 changes: 51 additions & 0 deletions src/TALXIS.CLI.Core/Contracts/Dataverse/DateFormats.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace TALXIS.CLI.Core.Contracts.Dataverse;

/// <summary>
/// Maps Dataverse date format strings to their codes
/// </summary>
public static class DateFormats
{
/// <summary>Short date formats mapped to their <c>dateformatcode</c>.</summary>
public static readonly IReadOnlyDictionary<string, int> Short = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["dd/MMMM/yy"] = 0,
["M/d/yy"] = 1,
["M/d/yyyy"] = 2,
["MM/dd/yy"] = 3,
["MM/dd/yyyy"] = 4,
["yy/MM/dd"] = 5,
["yyyy/MM/dd"] = 6,
};

/// <summary>Long date formats mapped to their <c>longdateformatcode</c>.</summary>
public static readonly IReadOnlyDictionary<string, int> Long = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["d MMMM, yyyy"] = 0,
["dddd, d MMMM, yyyy"] = 1,
["dddd, MMMM d, yyyy"] = 2,
["MMMM d, yyyy"] = 3,
};

/// <summary>
/// Turns user input into a format code.
/// </summary>
public static int ToCode(IReadOnlyDictionary<string, int> formats, string input, string label)
{
if (string.IsNullOrWhiteSpace(input)) throw new ArgumentException($"{label} cannot be empty.");


if (int.TryParse(input, out var code)) return code;


if (formats.TryGetValue(input, out var mapped)) return mapped;

var known = string.Join(Environment.NewLine, formats.OrderBy(pair => pair.Value).Select(pair => $" {pair.Value} {pair.Key}"));
throw new ArgumentException(
$"'{input}' is not a known {label}. Pass a code or one of:{Environment.NewLine}{known}");
}

/// <summary>
/// Returns the format string for a code, or null when the code is unknown.
/// </summary>
public static string? Describe(IReadOnlyDictionary<string, int> formats, int? code) => code is { } codeValue ? formats.FirstOrDefault(pair => pair.Value == codeValue).Key : null;
}
73 changes: 73 additions & 0 deletions src/TALXIS.CLI.Core/Contracts/Dataverse/IUserSettingsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
namespace TALXIS.CLI.Core.Contracts.Dataverse;

/// <summary>
/// The connected user's personalization settings (timezone, locale, date/time
/// formats, currency) read from the Dataverse <c>usersettings</c> entity.
/// </summary>
public sealed record UserSettingsInfo(
Guid UserId,
string? FullName,
string? Email,
int? TimeZoneCode,
string? TimeZoneName,
int? LocaleId,
string? LocaleName,
int? ShortDateFormatCode,
string? ShortDateFormat,
int? LongDateFormatCode,
string? TimeFormat,
string? CurrencyCode,
string? CurrencyName);

/// <summary>A Dataverse <c>timezonedefinition</c> row.</summary>
public sealed record TimezoneInfo(int Code, string Name, string? StandardName);

/// <summary>A Dataverse <c>transactioncurrency</c> row.</summary>
public sealed record CurrencyInfo(Guid Id, string IsoCode, string Name, string? Symbol);

/// <summary>
/// The subset of user settings to change. Null fields are left untouched.
/// </summary>
public sealed record UserSettingsUpdate(
int? TimeZoneCode = null,
int? LocaleId = null,
int? ShortDateFormatCode = null,
int? LongDateFormatCode = null,
string? TimeFormat = null,
Guid? CurrencyId = null)
{
public bool IsEmpty => TimeZoneCode is null && LocaleId is null && ShortDateFormatCode is null
&& LongDateFormatCode is null && TimeFormat is null && CurrencyId is null;
}

/// <summary>
/// Reads and updates the connected user's personalization settings and looks
/// up available timezones and currencies. Keyed off the current user resolved
/// via <c>WhoAmI</c>, so it always targets the profile's own account.
/// </summary>
public interface IUserSettingsService
{
/// <summary>Gets the connected user's current settings.</summary>
Task<UserSettingsInfo> GetCurrentAsync(string? profileName, CancellationToken ct);

/// <summary>Applies the non-null fields of <paramref name="update"/> to the connected user.</summary>
Task UpdateCurrentAsync(string? profileName, UserSettingsUpdate update, CancellationToken ct);

/// <summary>Lists timezones, optionally filtered by a substring of the display or standard name.</summary>
Task<IReadOnlyList<TimezoneInfo>> ListTimezonesAsync(string? profileName, string? filter, CancellationToken ct);

/// <summary>
/// Resolves a timezone name (fuzzy) to its numeric code. Throws
/// <see cref="ArgumentException"/> when nothing matches or the query is ambiguous.
/// </summary>
Task<int> ResolveTimezoneCodeAsync(string? profileName, string query, CancellationToken ct);

/// <summary>Lists currencies enabled in the environment, optionally filtered by a substring of the ISO code or name.</summary>
Task<IReadOnlyList<CurrencyInfo>> ListCurrenciesAsync(string? profileName, string? filter, CancellationToken ct);

/// <summary>
/// Resolves a currency (ISO code or name, fuzzy) to its record id. Throws
/// <see cref="ArgumentException"/> when nothing matches or the query is ambiguous.
/// </summary>
Task<Guid> ResolveCurrencyIdAsync(string? profileName, string query, CancellationToken ct);
}
21 changes: 21 additions & 0 deletions src/TALXIS.CLI.Features.Environment/Currency/CurrencyCliCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using DotMake.CommandLine;

namespace TALXIS.CLI.Features.Environment.Currency;

/// <summary>
/// <c>txc environment currency</c> - look up currencies enabled in the
/// environment for use with <c>user-settings set --currency</c>.
/// </summary>
[CliCommand(
Name = "currency",
Description = "Look up currencies enabled in the environment.",
Children = new[] { typeof(CurrencyListCliCommand) },
ShortFormAutoGenerate = CliNameAutoGenerate.None
)]
public class CurrencyCliCommand
{
public void Run(CliContext context)
{
context.ShowHelp();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using DotMake.CommandLine;
using Microsoft.Extensions.Logging;
using TALXIS.CLI.Core;
using TALXIS.CLI.Core.Contracts.Dataverse;
using TALXIS.CLI.Core.DependencyInjection;
using TALXIS.CLI.Logging;

namespace TALXIS.CLI.Features.Environment.Currency;

/// <summary>
/// <c>txc environment currency list</c> - lists currencies enabled in the
/// environment (ISO code + name), optionally filtered.
/// </summary>
[CliReadOnly]
[CliCommand(
Name = "list",
Description = "Lists currencies enabled in the LIVE connected environment (ISO code + name). Requires an active profile."
)]
public class CurrencyListCliCommand : ProfiledCliCommand
{
protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(nameof(CurrencyListCliCommand));

[CliOption(Name = "--filter", Description = "Show only currencies whose ISO code or name contains this substring.", Required = false)]
public string? Filter { get; set; }

protected override async Task<int> ExecuteAsync()
{
var service = TxcServices.Get<IUserSettingsService>();
var currencies = await service.ListCurrenciesAsync(Profile, Filter, CancellationToken.None).ConfigureAwait(false);

OutputFormatter.WriteList(currencies, PrintTable);
return ExitSuccess;
}

// Text-renderer callback invoked by OutputFormatter.WriteList; OutputWriter usage is intentional.
#pragma warning disable TXC003
private static void PrintTable(IReadOnlyList<CurrencyInfo> currencies)
{
if (currencies.Count == 0)
{
OutputWriter.WriteLine("No currencies found.");
return;
}

OutputWriter.WriteLine($"{"ISO",-5} | Name");
OutputWriter.WriteLine(new string('-', 60));
foreach (var currency in currencies)
{
OutputWriter.WriteLine($"{currency.IsoCode,-5} | {currency.Name}");
}
}
#pragma warning restore TXC003
}
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(EnvironmentListCliCommand), typeof(EnvironmentCreateCliCommand), typeof(EnvironmentUpdateCliCommand), typeof(EnvironmentDeleteCliCommand), 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) },
Children = new[] { typeof(EnvironmentListCliCommand), typeof(EnvironmentCreateCliCommand), typeof(EnvironmentUpdateCliCommand), typeof(EnvironmentDeleteCliCommand), 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), typeof(UserSettings.UserSettingsCliCommand), typeof(Timezone.TimezoneCliCommand), typeof(Currency.CurrencyCliCommand) },
ShortFormAutoGenerate = CliNameAutoGenerate.None
)]
public class EnvironmentCliCommand
Expand Down
21 changes: 21 additions & 0 deletions src/TALXIS.CLI.Features.Environment/Timezone/TimezoneCliCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using DotMake.CommandLine;

namespace TALXIS.CLI.Features.Environment.Timezone;

/// <summary>
/// <c>txc environment timezone</c> - look up Dataverse timezones for use with
/// <c>user-settings set --timezone</c>.
/// </summary>
[CliCommand(
Name = "timezone",
Description = "Look up Dataverse timezones.",
Children = new[] { typeof(TimezoneListCliCommand) },
ShortFormAutoGenerate = CliNameAutoGenerate.None
)]
public class TimezoneCliCommand
{
public void Run(CliContext context)
{
context.ShowHelp();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using DotMake.CommandLine;
using Microsoft.Extensions.Logging;
using TALXIS.CLI.Core;
using TALXIS.CLI.Core.Contracts.Dataverse;
using TALXIS.CLI.Core.DependencyInjection;
using TALXIS.CLI.Logging;

namespace TALXIS.CLI.Features.Environment.Timezone;

/// <summary>
/// <c>txc environment timezone list</c> - lists available timezones and their
/// codes, optionally filtered by name.
/// </summary>
[CliReadOnly]
[CliCommand(
Name = "list",
Description = "Lists Dataverse timezones (code + name) from the LIVE connected environment. Requires an active profile."
)]
public class TimezoneListCliCommand : ProfiledCliCommand
{
protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(nameof(TimezoneListCliCommand));

[CliOption(Name = "--filter", Description = "Show only timezones whose name contains this substring.", Required = false)]
public string? Filter { get; set; }

protected override async Task<int> ExecuteAsync()
{
var service = TxcServices.Get<IUserSettingsService>();
var timezones = await service.ListTimezonesAsync(Profile, Filter, CancellationToken.None).ConfigureAwait(false);

OutputFormatter.WriteList(timezones, PrintTable);
return ExitSuccess;
}

// Text-renderer callback invoked by OutputFormatter.WriteList; OutputWriter usage is intentional.
#pragma warning disable TXC003
private static void PrintTable(IReadOnlyList<TimezoneInfo> timezones)
{
if (timezones.Count == 0)
{
OutputWriter.WriteLine("No timezones found.");
return;
}

OutputWriter.WriteLine($"{"Code",-5} | Name");
OutputWriter.WriteLine(new string('-', 60));
foreach (var timezone in timezones)
{
OutputWriter.WriteLine($"{timezone.Code,-5} | {timezone.Name}");
}
}
#pragma warning restore TXC003
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using DotMake.CommandLine;

namespace TALXIS.CLI.Features.Environment.UserSettings;

/// <summary>
/// <c>txc environment user-settings</c> - view and change the connected
/// user's personalization settings (timezone, locale, date/time formats).
/// </summary>
[CliCommand(
Name = "user-settings",
Description = "View and change the connected user's settings (timezone, locale, date/time formats).",
Children = new[] { typeof(UserSettingsGetCliCommand), typeof(UserSettingsSetCliCommand) },
ShortFormAutoGenerate = CliNameAutoGenerate.None
)]
public class UserSettingsCliCommand
{
public void Run(CliContext context)
{
context.ShowHelp();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using DotMake.CommandLine;
using Microsoft.Extensions.Logging;
using TALXIS.CLI.Core;
using TALXIS.CLI.Core.Contracts.Dataverse;
using TALXIS.CLI.Core.DependencyInjection;
using TALXIS.CLI.Logging;

namespace TALXIS.CLI.Features.Environment.UserSettings;

/// <summary>
/// <c>txc environment user-settings get</c> - shows the connected user's
/// timezone, locale, date/time formats, and currency.
/// </summary>
[CliReadOnly]
[CliCommand(
Name = "get",
Description = "Shows the connected user's settings (timezone, locale, date/time formats, currency) from the LIVE connected environment. Requires an active profile."
)]
public class UserSettingsGetCliCommand : ProfiledCliCommand
{
protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(nameof(UserSettingsGetCliCommand));

protected override async Task<int> ExecuteAsync()
{
var service = TxcServices.Get<IUserSettingsService>();
var settings = await service.GetCurrentAsync(Profile, CancellationToken.None).ConfigureAwait(false);

OutputFormatter.WriteData(settings, Print);
return ExitSuccess;
}

// Text-renderer callback invoked by OutputFormatter.WriteData; OutputWriter usage is intentional.
#pragma warning disable TXC003
private static void Print(UserSettingsInfo userSettingsInfo)
{
var user = userSettingsInfo.FullName is { } name
? (userSettingsInfo.Email is { } email ? $"{name} ({email})" : name)
: userSettingsInfo.UserId.ToString();
var timezone = userSettingsInfo.TimeZoneCode is { } code
? (userSettingsInfo.TimeZoneName is { } tzName ? $"{tzName} (code: {code})" : $"code {code}")
: "(not set)";
var locale = userSettingsInfo.LocaleId is { } localeId
? (userSettingsInfo.LocaleName is { } localeName ? $"{localeName} (code: {localeId})" : $"code {localeId}")
: "(not set)";
var currency = userSettingsInfo.CurrencyCode is { } isoCode
? (userSettingsInfo.CurrencyName is { } currencyName ? $"{isoCode} ({currencyName})" : isoCode)
: "(not set)";
var shortDate = FormatCode(userSettingsInfo.ShortDateFormatCode, userSettingsInfo.ShortDateFormat ?? DateFormats.Describe(DateFormats.Short, userSettingsInfo.ShortDateFormatCode));
var longDate = FormatCode(userSettingsInfo.LongDateFormatCode, DateFormats.Describe(DateFormats.Long, userSettingsInfo.LongDateFormatCode));

OutputWriter.WriteLine($"User: {user}");
OutputWriter.WriteLine($"Timezone: {timezone}");
OutputWriter.WriteLine($"Locale: {locale}");
OutputWriter.WriteLine($"Short date: {shortDate}");
OutputWriter.WriteLine($"Long date: {longDate}");
OutputWriter.WriteLine($"Time format: {userSettingsInfo.TimeFormat ?? "(not set)"}");
OutputWriter.WriteLine($"Currency: {currency}");
}

private static string FormatCode(int? code, string? description)
{
if (code is not { } codeValue)
return "(not set)";
return description is { } format ? $"{format} (code: {codeValue})" : codeValue.ToString();
}
#pragma warning restore TXC003
}
Loading