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
45 changes: 45 additions & 0 deletions src/Netclaw.Cli.Tests/Tui/Wizard/DiscordStepViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ public void SubStepCount_IsFive_WhenEnabled()
Assert.Equal(5, step.SubStepCount);
}

[Fact]
public void SubStepCount_IsSix_WhenEnabled_WithRestrict()
{
using var step = new DiscordStepViewModel(_fakeProbe);
step.DiscordEnabled = true;
step.RestrictToSpecificUsers = true;
Assert.Equal(6, step.SubStepCount);
}

[Fact]
public void TryAdvance_ThroughAllSubSteps()
{
Expand All @@ -68,7 +77,43 @@ public void TryAdvance_ThroughAllSubSteps()
Assert.True(step.TryAdvance());
Assert.Equal(4, step.CurrentSubStep);

// Sub-step 4 is UserAccessChoice; default RestrictToSpecificUsers=false completes the step
Assert.False(step.TryAdvance());
}

[Fact]
public void TryAdvance_WithRestrict_AdvancesToAllowedUserIds()
{
using var step = new DiscordStepViewModel(_fakeProbe);
step.DiscordEnabled = true;

// Advance to sub-step 4 (UserAccessChoice)
for (var i = 0; i < 4; i++)
step.TryAdvance();
Assert.Equal(4, step.CurrentSubStep);

step.RestrictToSpecificUsers = true;
Assert.True(step.TryAdvance());
Assert.Equal(5, step.CurrentSubStep);

// Sub-step 5 (AllowedUserIds) completes the step
Assert.False(step.TryAdvance());
}

[Fact]
public void TryAdvance_AllowAnyone_ClearsAllowedUserIds()
{
using var step = new DiscordStepViewModel(_fakeProbe);
step.DiscordEnabled = true;
step.AllowedUserIdsInput = "129847561203948576";

// Advance to sub-step 4 (UserAccessChoice)
for (var i = 0; i < 4; i++)
step.TryAdvance();

step.RestrictToSpecificUsers = false;
Assert.False(step.TryAdvance());
Assert.Null(step.AllowedUserIdsInput);
}

[Fact]
Expand Down
46 changes: 45 additions & 1 deletion src/Netclaw.Cli.Tests/Tui/Wizard/SlackStepViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ public void SubStepCount_IsSix_WhenEnabled()
Assert.Equal(6, step.SubStepCount);
}

[Fact]
public void SubStepCount_IsSeven_WhenEnabled_WithRestrict()
{
using var step = new SlackStepViewModel(_fakeProbe);
step.SlackEnabled = true;
step.RestrictToSpecificUsers = true;
Assert.Equal(7, step.SubStepCount);
}

[Fact]
public void TryAdvance_ReturnsFalse_WhenDisabled()
{
Expand Down Expand Up @@ -84,8 +93,43 @@ public void TryAdvance_ThroughAllSubSteps()
Assert.True(step.TryAdvance());
Assert.Equal(5, step.CurrentSubStep);

// 5 → complete
// Sub-step 5 is UserAccessChoice; default RestrictToSpecificUsers=false completes the step
Assert.False(step.TryAdvance());
}

[Fact]
public void TryAdvance_WithRestrict_AdvancesToAllowedUserIds()
{
using var step = new SlackStepViewModel(_fakeProbe);
step.SlackEnabled = true;

// Advance to sub-step 5 (UserAccessChoice)
for (var i = 0; i < 5; i++)
step.TryAdvance();
Assert.Equal(5, step.CurrentSubStep);

step.RestrictToSpecificUsers = true;
Assert.True(step.TryAdvance());
Assert.Equal(6, step.CurrentSubStep);

// Sub-step 6 (AllowedUserIds) completes the step
Assert.False(step.TryAdvance());
}

[Fact]
public void TryAdvance_AllowAnyone_ClearsAllowedUserIds()
{
using var step = new SlackStepViewModel(_fakeProbe);
step.SlackEnabled = true;
step.AllowedUserIdsInput = "U01ABC123";

// Advance to sub-step 5 (UserAccessChoice)
for (var i = 0; i < 5; i++)
step.TryAdvance();

step.RestrictToSpecificUsers = false;
Assert.False(step.TryAdvance());
Assert.Null(step.AllowedUserIdsInput);
}

[Fact]
Expand Down
24 changes: 20 additions & 4 deletions src/Netclaw.Cli/Tui/Wizard/Steps/DiscordStepView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ namespace Netclaw.Cli.Tui.Wizard.Steps;

/// <summary>
/// Termina view for the Discord wizard step.
/// 5 sub-steps: enable -> bot token -> channel IDs -> DM enabled -> allowed user IDs.
/// 6 sub-steps: enable -> bot token -> channel IDs -> DM enabled -> user access choice -> allowed user IDs (conditional).
/// </summary>
public sealed class DiscordStepView : IWizardStepView
{
private SelectionListNode<string>? _enabledList;
private TextInputNode? _botTokenInput;
private TextInputNode? _channelIdsInput;
private SelectionListNode<string>? _dmEnabledList;
private SelectionListNode<string>? _userAccessChoiceList;
private TextInputNode? _allowedUserIdsInput;
private IFocusable? _lastFocusedList;
private TextInputBaseNode? _lastFocusedInput;
Expand All @@ -34,7 +35,8 @@ public ILayoutNode BuildContent(IWizardStepViewModel stepVm, StepViewCallbacks c
1 => BuildBotTokenSubStep(vm, callbacks),
2 => BuildChannelIdsSubStep(vm, callbacks),
3 => BuildDmEnabledSubStep(vm, callbacks),
4 => BuildAllowedUserIdsSubStep(vm, callbacks),
4 => BuildUserAccessChoiceSubStep(vm, callbacks),
5 => BuildAllowedUserIdsSubStep(vm, callbacks),
_ => Layouts.Empty()
};
}
Expand Down Expand Up @@ -156,6 +158,18 @@ private ILayoutNode BuildDmEnabledSubStep(DiscordStepViewModel vm, StepViewCallb
.WithChild(_dmEnabledList);
}

private ILayoutNode BuildUserAccessChoiceSubStep(DiscordStepViewModel vm, StepViewCallbacks callbacks)
{
var (list, layout) = WizardStepHelpers.BuildUserAccessChoiceSubStep(
restrict => vm.RestrictToSpecificUsers = restrict, callbacks);

_userAccessChoiceList = list;
_lastFocusedList = list;
_lastFocusedInput = null;

return layout;
}

private ILayoutNode BuildAllowedUserIdsSubStep(DiscordStepViewModel vm, StepViewCallbacks callbacks)
{
_allowedUserIdsInput = new TextInputNode()
Expand All @@ -169,15 +183,16 @@ private ILayoutNode BuildAllowedUserIdsSubStep(DiscordStepViewModel vm, StepView
_lastFocusedList = null;

_allowedUserIdsInput.Submitted
.Where(text => !string.IsNullOrWhiteSpace(text))
.Subscribe(text =>
{
vm.AllowedUserIdsInput = string.IsNullOrWhiteSpace(text) ? null : text;
vm.AllowedUserIdsInput = text;
callbacks.AdvanceStep();
})
.DisposeWith(callbacks.Subscriptions);

return Layouts.Vertical()
.WithChild(new TextNode(" Allowed user IDs (press Enter to skip):").WithForeground(Color.White))
.WithChild(new TextNode(" Allowed user IDs (at least one required):").WithForeground(Color.White))
.WithChild(new PanelNode()
.WithTitle("Allowed User IDs")
.WithBorder(BorderStyle.Rounded)
Expand Down Expand Up @@ -216,6 +231,7 @@ public void ClearFocusState()
_botTokenInput = null;
_channelIdsInput = null;
_dmEnabledList = null;
_userAccessChoiceList = null;
_allowedUserIdsInput = null;
}
}
33 changes: 23 additions & 10 deletions src/Netclaw.Cli/Tui/Wizard/Steps/DiscordStepViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Netclaw.Cli.Tui.Wizard.Steps;

/// <summary>
/// Wizard step for configuring Discord integration.
/// 5 sub-steps: enable -> bot token -> allowed channel IDs -> DM enabled -> allowed user IDs.
/// 6 sub-steps: enable -> bot token -> allowed channel IDs -> DM enabled -> user access choice -> allowed user IDs (conditional).
/// </summary>
public sealed class DiscordStepViewModel : IWizardStepViewModel, IChannelAdapterViewModel
{
Expand Down Expand Up @@ -38,6 +38,7 @@ bool IChannelAdapterViewModel.AdapterEnabled
public string? BotToken { get; set; }
public string? ChannelIdsInput { get; set; }
public bool AllowDirectMessages { get; set; }
public bool RestrictToSpecificUsers { get; set; }
public string? AllowedUserIdsInput { get; set; }
internal DiscordChannelResolutionResult? LastChannelResolution { get; set; }

Expand All @@ -46,15 +47,18 @@ bool IChannelAdapterViewModel.AdapterEnabled
public bool IsApplicable(WizardContext context) => true;

public int CurrentSubStep => _currentSubStep;
public int SubStepCount => DiscordEnabled ? (SkipEnableSubStep ? 4 : 5) : 1;
public int SubStepCount => DiscordEnabled
? (SkipEnableSubStep ? 4 : 5) + (RestrictToSpecificUsers ? 1 : 0)
: 1;

public string GetHelpText() => _currentSubStep switch
{
0 => " Enable Discord to connect Netclaw with a bot token.",
1 => " Enter the Discord bot token from your application settings.",
2 => " Allowed channel IDs are comma-separated. Leave blank for no guild channel ingress.",
3 => " Enable DMs only when you want Discord direct messages to be accepted.",
4 => " Restrict Discord DM/message access to specific user IDs. Leave blank to allow any user in allowed conversations.",
4 => " Choose whether to restrict bot interactions to specific Discord user IDs.",
5 => " Enter the Discord user IDs who should have access. At least one ID is required.",
_ => ""
};

Expand All @@ -77,6 +81,20 @@ public bool TryAdvance()
return true;
}

if (_currentSubStep == 4 && DiscordEnabled)
{
if (RestrictToSpecificUsers)
{
_currentSubStep = 5;
_highWaterSubStep = 5;
return true;
}

AllowedUserIdsInput = null;
_highWaterSubStep = 4;
return false;
}

return false;
}

Expand Down Expand Up @@ -110,6 +128,7 @@ internal void ResetConfig()
BotToken = null;
ChannelIdsInput = null;
AllowDirectMessages = false;
RestrictToSpecificUsers = false;
AllowedUserIdsInput = null;
LastChannelResolution = null;
var startSubStep = SkipEnableSubStep ? 1 : 0;
Expand Down Expand Up @@ -366,13 +385,7 @@ internal static List<string> ParseChannelIds(string? input)
.ToList();

private static List<string> ParseUserIds(string? input)
=> string.IsNullOrWhiteSpace(input)
? []
: input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(x => x.Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct(StringComparer.Ordinal)
.ToList();
=> WizardStepHelpers.ParseUserIds(input);

public void Dispose()
{
Expand Down
24 changes: 20 additions & 4 deletions src/Netclaw.Cli/Tui/Wizard/Steps/SlackStepView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Netclaw.Cli.Tui.Wizard.Steps;

/// <summary>
/// Termina view for the Slack wizard step.
/// 6 sub-steps: enable → bot token → app token → channel names → DM → user IDs.
/// 7 sub-steps: enable → bot token → app token → channel names → DM → user access choice → user IDs (conditional).
/// </summary>
public sealed class SlackStepView : IWizardStepView
{
Expand All @@ -19,6 +19,7 @@ public sealed class SlackStepView : IWizardStepView
private TextInputNode? _appTokenInput;
private TextInputNode? _channelNamesInput;
private SelectionListNode<string>? _dmEnabledList;
private SelectionListNode<string>? _userAccessChoiceList;
private TextInputNode? _allowedUserIdsInput;
private IFocusable? _lastFocusedList;
private TextInputBaseNode? _lastFocusedInput;
Expand All @@ -36,7 +37,8 @@ public ILayoutNode BuildContent(IWizardStepViewModel stepVm, StepViewCallbacks c
2 => BuildAppTokenSubStep(vm, callbacks),
3 => BuildChannelNamesSubStep(vm, callbacks),
4 => BuildDmEnabledSubStep(vm, callbacks),
5 => BuildAllowedUserIdsSubStep(vm, callbacks),
5 => BuildUserAccessChoiceSubStep(vm, callbacks),
6 => BuildAllowedUserIdsSubStep(vm, callbacks),
_ => Layouts.Empty()
};
}
Expand Down Expand Up @@ -197,6 +199,18 @@ private ILayoutNode BuildDmEnabledSubStep(SlackStepViewModel vm, StepViewCallbac
.WithChild(_dmEnabledList);
}

private ILayoutNode BuildUserAccessChoiceSubStep(SlackStepViewModel vm, StepViewCallbacks callbacks)
{
var (list, layout) = WizardStepHelpers.BuildUserAccessChoiceSubStep(
restrict => vm.RestrictToSpecificUsers = restrict, callbacks);

_userAccessChoiceList = list;
_lastFocusedList = list;
_lastFocusedInput = null;

return layout;
}

private ILayoutNode BuildAllowedUserIdsSubStep(SlackStepViewModel vm, StepViewCallbacks callbacks)
{
_allowedUserIdsInput = new TextInputNode()
Expand All @@ -210,15 +224,16 @@ private ILayoutNode BuildAllowedUserIdsSubStep(SlackStepViewModel vm, StepViewCa
_lastFocusedList = null;

_allowedUserIdsInput.Submitted
.Where(text => !string.IsNullOrWhiteSpace(text))
.Subscribe(text =>
{
vm.AllowedUserIdsInput = string.IsNullOrWhiteSpace(text) ? null : text;
vm.AllowedUserIdsInput = text;
callbacks.AdvanceStep();
})
.DisposeWith(callbacks.Subscriptions);

return Layouts.Vertical()
.WithChild(new TextNode(" Allowed user IDs (press Enter to skip):").WithForeground(Color.White))
.WithChild(new TextNode(" Allowed user IDs (at least one required):").WithForeground(Color.White))
.WithChild(new PanelNode()
.WithTitle("Allowed User IDs")
.WithBorder(BorderStyle.Rounded)
Expand Down Expand Up @@ -256,6 +271,7 @@ public void ClearFocusState()
_appTokenInput = null;
_channelNamesInput = null;
_dmEnabledList = null;
_userAccessChoiceList = null;
_allowedUserIdsInput = null;
}
}
Loading