Skip to content

Commit 6eebf73

Browse files
authored
feat: implement main tool window and connection dialog (#314)
* feat: implement main tool window and connection dialog Tool Window (#310): - Add MVVM structure with ViewModels for all tree node types - Implement toolbar with Add Connection, Refresh, Collapse All - Add TreeView with HierarchicalDataTemplates for each node type - Implement context menus for Connection, Bucket, Scope, Collection, Document - Add VS theme support using VsBrushes - Add empty state message when no connections exist Connection Dialog (#311): - Create VS-styled dialog following Add Connection pattern - Add Connection Type selection (Server vs Capella) - Implement field validation with inline error messages - Add Test Connection with status indicator (Not tested/Testing/Success/Failed) - Auto-detect Capella URLs and enforce SSL - Support both Add and Edit modes - Wire up Add/Edit/Delete connection commands Other: - Update CouchbaseNetClient from 3.5.2 to 3.8.1 (security fix) * fix: escape curly braces in XAML document icon
1 parent f03ca46 commit 6eebf73

20 files changed

+1504
-11
lines changed

src/CodingWithCalvin.CouchbaseExplorer/CodingWithCalvin.CouchbaseExplorer.csproj

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,30 @@
7474
<DesignTime>True</DesignTime>
7575
<DependentUpon>VSCommandTable.vsct</DependentUpon>
7676
</Compile>
77+
<Compile Include="ViewModels\BucketNode.cs" />
78+
<Compile Include="ViewModels\BucketsFolderNode.cs" />
79+
<Compile Include="ViewModels\CollectionNode.cs" />
80+
<Compile Include="ViewModels\ConnectionNode.cs" />
81+
<Compile Include="ViewModels\ConnectionDialogViewModel.cs" />
82+
<Compile Include="ViewModels\Converters.cs" />
83+
<Compile Include="ViewModels\CouchbaseExplorerViewModel.cs" />
84+
<Compile Include="ViewModels\DocumentNode.cs" />
85+
<Compile Include="ViewModels\IndexesFolderNode.cs" />
86+
<Compile Include="ViewModels\IndexNode.cs" />
87+
<Compile Include="ViewModels\LoadMoreNode.cs" />
88+
<Compile Include="ViewModels\PlaceholderNode.cs" />
89+
<Compile Include="ViewModels\RelayCommand.cs" />
90+
<Compile Include="ViewModels\ScopeNode.cs" />
91+
<Compile Include="ViewModels\TreeNodeBase.cs" />
92+
<Compile Include="Dialogs\ConnectionDialog.xaml.cs">
93+
<DependentUpon>ConnectionDialog.xaml</DependentUpon>
94+
</Compile>
95+
</ItemGroup>
96+
<ItemGroup>
97+
<Page Include="Dialogs\ConnectionDialog.xaml">
98+
<SubType>Designer</SubType>
99+
<Generator>MSBuild:Compile</Generator>
100+
</Page>
77101
</ItemGroup>
78102
<ItemGroup>
79103
<Content Include="..\..\LICENSE">
@@ -125,7 +149,7 @@
125149
</ItemGroup>
126150
<ItemGroup>
127151
<PackageReference Include="CouchbaseNetClient">
128-
<Version>3.5.2</Version>
152+
<Version>3.8.1</Version>
129153
</PackageReference>
130154
<PackageReference Include="Microsoft.VisualStudio.SDK">
131155
<Version>17.9.37000</Version>
Lines changed: 252 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,263 @@
1-
<UserControl x:Class="CodingWithCalvin.CouchbaseExplorer.CouchbaseExplorerWindowControl"
1+
<UserControl x:Class="CodingWithCalvin.CouchbaseExplorer.CouchbaseExplorerWindowControl"
22
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
33
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
44
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
55
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
66
xmlns:vsshell="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0"
7+
xmlns:vm="clr-namespace:CodingWithCalvin.CouchbaseExplorer.ViewModels"
78
Background="{DynamicResource {x:Static vsshell:VsBrushes.WindowKey}}"
89
Foreground="{DynamicResource {x:Static vsshell:VsBrushes.WindowTextKey}}"
910
mc:Ignorable="d"
10-
d:DesignHeight="300" d:DesignWidth="300"
11-
Name="MyToolWindow">
11+
d:DesignHeight="400" d:DesignWidth="300"
12+
Name="CouchbaseExplorerControl">
13+
14+
<UserControl.Resources>
15+
<!-- TreeView Item Style for proper selection binding -->
16+
<Style TargetType="TreeViewItem">
17+
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
18+
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
19+
<Setter Property="Foreground" Value="{DynamicResource {x:Static vsshell:VsBrushes.WindowTextKey}}" />
20+
</Style>
21+
22+
<!-- Context Menu for Connections root (empty space) -->
23+
<ContextMenu x:Key="RootContextMenu">
24+
<MenuItem Header="Add Connection..." Command="{Binding AddConnectionCommand}" />
25+
</ContextMenu>
26+
27+
<!-- Context Menu for Disconnected Connection -->
28+
<ContextMenu x:Key="DisconnectedConnectionContextMenu">
29+
<MenuItem Header="Connect" Command="{Binding ConnectCommand}" />
30+
<Separator />
31+
<MenuItem Header="Edit Connection..." Command="{Binding EditConnectionCommand}" />
32+
<MenuItem Header="Delete Connection" Command="{Binding DeleteConnectionCommand}" />
33+
</ContextMenu>
34+
35+
<!-- Context Menu for Connected Connection -->
36+
<ContextMenu x:Key="ConnectedConnectionContextMenu">
37+
<MenuItem Header="Disconnect" Command="{Binding DisconnectCommand}" />
38+
<MenuItem Header="Refresh" Command="{Binding RefreshCommand}" />
39+
<Separator />
40+
<MenuItem Header="Edit Connection..." Command="{Binding EditConnectionCommand}" />
41+
<MenuItem Header="Delete Connection" Command="{Binding DeleteConnectionCommand}" />
42+
</ContextMenu>
43+
44+
<!-- Context Menu for Bucket -->
45+
<ContextMenu x:Key="BucketContextMenu">
46+
<MenuItem Header="Refresh" Command="{Binding RefreshCommand}" />
47+
</ContextMenu>
48+
49+
<!-- Context Menu for Scope -->
50+
<ContextMenu x:Key="ScopeContextMenu">
51+
<MenuItem Header="Refresh" Command="{Binding RefreshCommand}" />
52+
</ContextMenu>
53+
54+
<!-- Context Menu for Collection -->
55+
<ContextMenu x:Key="CollectionContextMenu">
56+
<MenuItem Header="New Document..." Command="{Binding NewDocumentCommand}" />
57+
<Separator />
58+
<MenuItem Header="Refresh" Command="{Binding RefreshCommand}" />
59+
</ContextMenu>
60+
61+
<!-- Context Menu for Document -->
62+
<ContextMenu x:Key="DocumentContextMenu">
63+
<MenuItem Header="Open" Command="{Binding OpenDocumentCommand}" />
64+
<Separator />
65+
<MenuItem Header="Copy Document ID" Command="{Binding CopyDocumentIdCommand}" />
66+
<Separator />
67+
<MenuItem Header="Delete" Command="{Binding DeleteDocumentCommand}" />
68+
</ContextMenu>
69+
70+
<!-- Data Template for Connection Node -->
71+
<HierarchicalDataTemplate DataType="{x:Type vm:ConnectionNode}" ItemsSource="{Binding Children}">
72+
<StackPanel Orientation="Horizontal">
73+
<TextBlock Text="" Margin="0,0,5,0" Visibility="{Binding IsConnected, Converter={StaticResource BoolToVisibilityConverter}, FallbackValue=Collapsed}" />
74+
<TextBlock Text="" Margin="0,0,5,0" Visibility="{Binding IsConnected, Converter={StaticResource InverseBoolToVisibilityConverter}, FallbackValue=Visible}" />
75+
<TextBlock Text="{Binding Name}" />
76+
</StackPanel>
77+
</HierarchicalDataTemplate>
78+
79+
<!-- Data Template for Buckets Folder Node -->
80+
<HierarchicalDataTemplate DataType="{x:Type vm:BucketsFolderNode}" ItemsSource="{Binding Children}">
81+
<StackPanel Orientation="Horizontal">
82+
<TextBlock Text="📁" Margin="0,0,5,0" />
83+
<TextBlock Text="{Binding Name}" />
84+
</StackPanel>
85+
</HierarchicalDataTemplate>
86+
87+
<!-- Data Template for Indexes Folder Node -->
88+
<HierarchicalDataTemplate DataType="{x:Type vm:IndexesFolderNode}" ItemsSource="{Binding Children}">
89+
<StackPanel Orientation="Horizontal">
90+
<TextBlock Text="📁" Margin="0,0,5,0" />
91+
<TextBlock Text="{Binding Name}" />
92+
</StackPanel>
93+
</HierarchicalDataTemplate>
94+
95+
<!-- Data Template for Bucket Node -->
96+
<HierarchicalDataTemplate DataType="{x:Type vm:BucketNode}" ItemsSource="{Binding Children}">
97+
<StackPanel Orientation="Horizontal">
98+
<TextBlock Text="🪣" Margin="0,0,5,0" />
99+
<TextBlock Text="{Binding Name}" />
100+
</StackPanel>
101+
</HierarchicalDataTemplate>
102+
103+
<!-- Data Template for Scope Node -->
104+
<HierarchicalDataTemplate DataType="{x:Type vm:ScopeNode}" ItemsSource="{Binding Children}">
105+
<StackPanel Orientation="Horizontal">
106+
<TextBlock Text="📂" Margin="0,0,5,0" />
107+
<TextBlock Text="{Binding Name}" />
108+
</StackPanel>
109+
</HierarchicalDataTemplate>
110+
111+
<!-- Data Template for Collection Node -->
112+
<HierarchicalDataTemplate DataType="{x:Type vm:CollectionNode}" ItemsSource="{Binding Children}">
113+
<StackPanel Orientation="Horizontal">
114+
<TextBlock Text="📄" Margin="0,0,5,0" />
115+
<TextBlock Text="{Binding Name}" />
116+
</StackPanel>
117+
</HierarchicalDataTemplate>
118+
119+
<!-- Data Template for Document Node -->
120+
<DataTemplate DataType="{x:Type vm:DocumentNode}">
121+
<StackPanel Orientation="Horizontal">
122+
<TextBlock Text="{}{ }" Margin="0,0,5,0" FontFamily="Consolas" />
123+
<TextBlock Text="{Binding Name}" />
124+
</StackPanel>
125+
</DataTemplate>
126+
127+
<!-- Data Template for Index Node -->
128+
<DataTemplate DataType="{x:Type vm:IndexNode}">
129+
<StackPanel Orientation="Horizontal">
130+
<TextBlock Text="" Margin="0,0,5,0" />
131+
<TextBlock Text="{Binding Name}" />
132+
</StackPanel>
133+
</DataTemplate>
134+
135+
<!-- Data Template for Placeholder Node (Loading...) -->
136+
<DataTemplate DataType="{x:Type vm:PlaceholderNode}">
137+
<TextBlock Text="{Binding Name}" FontStyle="Italic"
138+
Foreground="{DynamicResource {x:Static vsshell:VsBrushes.GrayTextKey}}" />
139+
</DataTemplate>
140+
141+
<!-- Data Template for Load More Node -->
142+
<DataTemplate DataType="{x:Type vm:LoadMoreNode}">
143+
<TextBlock Text="{Binding Name}" FontStyle="Italic" TextDecorations="Underline" Cursor="Hand"
144+
Foreground="{DynamicResource {x:Static vsshell:VsBrushes.HighlightKey}}" />
145+
</DataTemplate>
146+
147+
<!-- Bool to Visibility Converter -->
148+
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
149+
150+
<!-- Inverse Bool to Visibility Converter -->
151+
<vm:InverseBooleanToVisibilityConverter x:Key="InverseBoolToVisibilityConverter" />
152+
153+
<!-- Count to Visibility Converter (shows when count is 0) -->
154+
<vm:CountToVisibilityConverter x:Key="CountToVisibilityConverter" />
155+
</UserControl.Resources>
156+
12157
<Grid>
13-
<StackPanel Orientation="Vertical">
14-
<TextBlock Margin="10" HorizontalAlignment="Center">Couchbase Explorer</TextBlock>
15-
<TreeView x:Name="ServersTreeView" Height="240">
16-
</TreeView>
17-
</StackPanel>
158+
<Grid.RowDefinitions>
159+
<RowDefinition Height="Auto" />
160+
<RowDefinition Height="*" />
161+
</Grid.RowDefinitions>
162+
163+
<!-- Toolbar -->
164+
<ToolBar Grid.Row="0"
165+
Background="{DynamicResource {x:Static vsshell:VsBrushes.CommandBarGradientKey}}">
166+
<Button Command="{Binding AddConnectionCommand}" ToolTip="Add Connection">
167+
<TextBlock Text="" />
168+
</Button>
169+
<Button Command="{Binding RefreshCommand}" ToolTip="Refresh">
170+
<TextBlock Text="🔄" />
171+
</Button>
172+
<Button Command="{Binding CollapseAllCommand}" ToolTip="Collapse All">
173+
<TextBlock Text="" />
174+
</Button>
175+
</ToolBar>
176+
177+
<!-- Tree View -->
178+
<TreeView Grid.Row="1"
179+
x:Name="ExplorerTreeView"
180+
ItemsSource="{Binding Connections}"
181+
Background="{DynamicResource {x:Static vsshell:VsBrushes.ToolWindowBackgroundKey}}"
182+
BorderThickness="0"
183+
ContextMenu="{StaticResource RootContextMenu}"
184+
VirtualizingStackPanel.IsVirtualizing="True"
185+
VirtualizingStackPanel.VirtualizationMode="Recycling">
186+
<TreeView.ItemContainerStyle>
187+
<Style TargetType="TreeViewItem">
188+
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
189+
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
190+
<Setter Property="Foreground" Value="{DynamicResource {x:Static vsshell:VsBrushes.WindowTextKey}}" />
191+
<Style.Triggers>
192+
<DataTrigger Binding="{Binding NodeType}" Value="Connection">
193+
<Setter Property="ContextMenu">
194+
<Setter.Value>
195+
<ContextMenu>
196+
<MenuItem Header="Connect" Command="{Binding DataContext.ConnectCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" />
197+
<MenuItem Header="Disconnect" Command="{Binding DataContext.DisconnectCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" />
198+
<Separator />
199+
<MenuItem Header="Refresh" Command="{Binding DataContext.RefreshCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" />
200+
<Separator />
201+
<MenuItem Header="Edit Connection..." Command="{Binding DataContext.EditConnectionCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" />
202+
<MenuItem Header="Delete Connection" Command="{Binding DataContext.DeleteConnectionCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" />
203+
</ContextMenu>
204+
</Setter.Value>
205+
</Setter>
206+
</DataTrigger>
207+
<DataTrigger Binding="{Binding NodeType}" Value="Bucket">
208+
<Setter Property="ContextMenu">
209+
<Setter.Value>
210+
<ContextMenu>
211+
<MenuItem Header="Refresh" Command="{Binding DataContext.RefreshCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" />
212+
</ContextMenu>
213+
</Setter.Value>
214+
</Setter>
215+
</DataTrigger>
216+
<DataTrigger Binding="{Binding NodeType}" Value="Scope">
217+
<Setter Property="ContextMenu">
218+
<Setter.Value>
219+
<ContextMenu>
220+
<MenuItem Header="Refresh" Command="{Binding DataContext.RefreshCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" />
221+
</ContextMenu>
222+
</Setter.Value>
223+
</Setter>
224+
</DataTrigger>
225+
<DataTrigger Binding="{Binding NodeType}" Value="Collection">
226+
<Setter Property="ContextMenu">
227+
<Setter.Value>
228+
<ContextMenu>
229+
<MenuItem Header="New Document..." Command="{Binding DataContext.NewDocumentCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" />
230+
<Separator />
231+
<MenuItem Header="Refresh" Command="{Binding DataContext.RefreshCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" />
232+
</ContextMenu>
233+
</Setter.Value>
234+
</Setter>
235+
</DataTrigger>
236+
<DataTrigger Binding="{Binding NodeType}" Value="Document">
237+
<Setter Property="ContextMenu">
238+
<Setter.Value>
239+
<ContextMenu>
240+
<MenuItem Header="Open" Command="{Binding DataContext.OpenDocumentCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" />
241+
<Separator />
242+
<MenuItem Header="Copy Document ID" Command="{Binding DataContext.CopyDocumentIdCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" />
243+
<Separator />
244+
<MenuItem Header="Delete" Command="{Binding DataContext.DeleteDocumentCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" />
245+
</ContextMenu>
246+
</Setter.Value>
247+
</Setter>
248+
</DataTrigger>
249+
</Style.Triggers>
250+
</Style>
251+
</TreeView.ItemContainerStyle>
252+
</TreeView>
253+
254+
<!-- Empty State -->
255+
<TextBlock Grid.Row="1"
256+
Text="Right-click to add a connection"
257+
HorizontalAlignment="Center"
258+
VerticalAlignment="Center"
259+
FontStyle="Italic"
260+
Foreground="{DynamicResource {x:Static vsshell:VsBrushes.GrayTextKey}}"
261+
Visibility="{Binding Connections.Count, Converter={StaticResource CountToVisibilityConverter}, FallbackValue=Visible}" />
18262
</Grid>
19263
</UserControl>
Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
1-
using System.Windows.Controls;
1+
using System.Windows.Controls;
2+
using CodingWithCalvin.CouchbaseExplorer.ViewModels;
23

34
namespace CodingWithCalvin.CouchbaseExplorer
45
{
56
public partial class CouchbaseExplorerWindowControl : UserControl
67
{
8+
public CouchbaseExplorerViewModel ViewModel { get; }
9+
710
public CouchbaseExplorerWindowControl()
811
{
9-
this.InitializeComponent();
12+
ViewModel = new CouchbaseExplorerViewModel();
13+
DataContext = ViewModel;
14+
15+
InitializeComponent();
16+
17+
ExplorerTreeView.SelectedItemChanged += OnSelectedItemChanged;
18+
}
19+
20+
private void OnSelectedItemChanged(object sender, System.Windows.RoutedPropertyChangedEventArgs<object> e)
21+
{
22+
if (e.NewValue is TreeNodeBase node)
23+
{
24+
ViewModel.SelectedNode = node;
25+
}
1026
}
1127
}
1228
}

0 commit comments

Comments
 (0)