Skip to content

Commit 8659015

Browse files
committed
Implement force delete for languages and categories.
This commit introduces the ability to forcibly delete a language or a category, including all of its associated child items (categories and/or snippets).
1 parent 27092c1 commit 8659015

File tree

5 files changed

+214
-14
lines changed

5 files changed

+214
-14
lines changed

src/CodeSnip/MainViewModel.cs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -554,10 +554,34 @@ private void OpenLanguageCategory()
554554
LoadSnippets();
555555
if (tmpSnippet != null)
556556
{
557-
ExpandAndSelectSnippet(
558-
tmpSnippet.Category?.Language?.Id ?? 0,
559-
tmpSnippet.CategoryId,
560-
tmpSnippet.Id);
557+
// This block handles the edge case where the currently active snippet
558+
// might have been deleted (e.g., via "Force Delete Category") inside the Language/Category editor.
559+
560+
// Flatten the entire collection of snippets from the newly loaded data and check if our snippet's ID is still present.
561+
bool snippetStillExists = Languages
562+
.SelectMany(l => l.Categories)
563+
.SelectMany(c => c.Snippets)
564+
.Any(s => s.Id == tmpSnippet.Id);
565+
566+
if (snippetStillExists)
567+
{
568+
// The snippet was not deleted. Restore the selection in the TreeView
569+
ExpandAndSelectSnippet(
570+
tmpSnippet.Category?.Language?.Id ?? 0,
571+
tmpSnippet.CategoryId,
572+
tmpSnippet.Id);
573+
}
574+
else
575+
{
576+
// The snippet was deleted. To prevent data inconsistency and potential crashes
577+
// from actions on a "phantom" snippet, we must reset the editor state completely.
578+
SelectedSnippet = null;
579+
EditingSnippet = null;
580+
EditorText = string.Empty;
581+
IsEditorModified = false;
582+
UpdateWindowTitle();
583+
StatusMessage = "The previously selected snippet was deleted.";
584+
}
561585
}
562586

563587
});

src/CodeSnip/Resources/Icons.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@
120120

121121
<PathGeometry x:Key="ImageSave">M22.71,6.29a1,1,0,0,0-1.42,0L20,7.59V2a1,1,0,0,0-2,0V7.59l-1.29-1.3a1,1,0,0,0-1.42,1.42l3,3a1,1,0,0,0,.33.21.94.94,0,0,0,.76,0,1,1,0,0,0,.33-.21l3-3A1,1,0,0,0,22.71,6.29ZM19,13a1,1,0,0,0-1,1v.38L16.52,12.9a2.79,2.79,0,0,0-3.93,0l-.7.7L9.41,11.12a2.85,2.85,0,0,0-3.93,0L4,12.6V7A1,1,0,0,1,5,6h8a1,1,0,0,0,0-2H5A3,3,0,0,0,2,7V19a3,3,0,0,0,3,3H17a3,3,0,0,0,3-3V14A1,1,0,0,0,19,13ZM5,20a1,1,0,0,1-1-1V15.43l2.9-2.9a.79.79,0,0,1,1.09,0l3.17,3.17,0,0L15.46,20Zm13-1a.89.89,0,0,1-.18.53L13.31,15l.7-.7a.77.77,0,0,1,1.1,0L18,17.21Z</PathGeometry>
122122

123+
<PathGeometry x:Key="DeleteAlert">M17 4V6H3V4H6.5L7.5 3H12.5L13.5 4H17M4 19V7H16V19C16 20.1 15.1 21 14 21H6C4.9 21 4 20.1 4 19M19 15H21V17H19V15M19 7H21V13H19V7Z</PathGeometry>
124+
123125

124126

125127
<Geometry x:Key="ContentCopy">M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z</Geometry>

src/CodeSnip/Services/DatabaseService.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,64 @@ public void DeleteCategory(int id)
358358
conn.Execute("DELETE FROM Categories WHERE ID = @Id", new { Id = id });
359359
}
360360

361+
public int CountSnippetsInCategory(int categoryId)
362+
{
363+
using var conn = CreateConnection();
364+
return conn.ExecuteScalar<int>("SELECT COUNT(*) FROM Snippets WHERE CategoryId = @CategoryId", new { CategoryId = categoryId });
365+
}
366+
367+
public int CountSnippetsInLanguage(int languageId)
368+
{
369+
using var conn = CreateConnection();
370+
return conn.ExecuteScalar<int>(@"
371+
SELECT COUNT(*)
372+
FROM Snippets s
373+
JOIN Categories c ON s.CategoryId = c.ID
374+
WHERE c.LanguageId = @LanguageId", new { LanguageId = languageId });
375+
}
376+
377+
public void ForceDeleteCategory(int categoryId)
378+
{
379+
using var conn = CreateConnection();
380+
conn.Open();
381+
using var transaction = conn.BeginTransaction();
382+
try
383+
{
384+
// Delete all snippete in category
385+
conn.Execute("DELETE FROM Snippets WHERE CategoryId = @CategoryId", new { CategoryId = categoryId }, transaction);
386+
// Delete the category itself
387+
conn.Execute("DELETE FROM Categories WHERE ID = @Id", new { Id = categoryId }, transaction);
388+
transaction.Commit();
389+
}
390+
catch
391+
{
392+
transaction.Rollback();
393+
throw;
394+
}
395+
}
396+
397+
public void ForceDeleteLanguage(int languageId)
398+
{
399+
using var conn = CreateConnection();
400+
conn.Open();
401+
using var transaction = conn.BeginTransaction();
402+
try
403+
{
404+
// Delete all snippets in categories of this language
405+
conn.Execute("DELETE FROM Snippets WHERE CategoryId IN (SELECT ID FROM Categories WHERE LanguageId = @LanguageId)", new { LanguageId = languageId }, transaction);
406+
// Delete all categories of this language
407+
conn.Execute("DELETE FROM Categories WHERE LanguageId = @LanguageId", new { LanguageId = languageId }, transaction);
408+
// Delete the language itself
409+
conn.Execute("DELETE FROM Languages WHERE ID = @Id", new { Id = languageId }, transaction);
410+
transaction.Commit();
411+
}
412+
catch
413+
{
414+
transaction.Rollback();
415+
throw;
416+
}
417+
}
418+
361419
public async Task<bool> RunIntegrityCheckAsync()
362420
{
363421
try

src/CodeSnip/Views/LanguageCategoryView/LanguageCategoryView.xaml

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
77
xmlns:local="clr-namespace:CodeSnip.Views.LanguageCategoryView"
88
xmlns:helpers="clr-namespace:CodeSnip.Helpers"
9-
mc:Ignorable="d"
10-
>
9+
d:DataContext="{d:DesignInstance Type={x:Type local:LanguageCategoryViewModel}}"
10+
mc:Ignorable="d" >
1111
<UserControl.Resources>
1212
<helpers:BoolToAddCancelConverter x:Key="BoolToAddCancelConverter"/>
1313
</UserControl.Resources>
@@ -37,13 +37,22 @@
3737
<ColumnDefinition Width="*"/>
3838
<ColumnDefinition Width="*" />
3939
<ColumnDefinition Width="*" />
40+
<ColumnDefinition Width="Auto" />
4041
</Grid.ColumnDefinitions>
4142
<Button Content="{Binding IsAddingLanguage, Converter={StaticResource BoolToAddCancelConverter}}"
4243
Command="{Binding ToggleAddLanguageCommand}" Grid.Column="0" Margin="0,0,10,0"/>
4344

4445
<Button Content="Save" Command="{Binding SaveLanguageCommand}" IsEnabled="{Binding SaveLanguageCommand.CanExecute}" Grid.Column="1" Margin="0,0,10,0" />
45-
<Button Content="Delete Language" Grid.Column="2"
46+
<Button Content="Delete Language" Grid.Column="2" Margin="0,0,10,0"
4647
Command="{Binding DeleteLanguageCommand}" />
48+
<Button Grid.Column="3" Command="{Binding ForceDeleteLanguageCommand}"
49+
ToolTip="Deletes the language and ALL its categories and snippets"
50+
ToolTipService.InitialShowDelay="300"
51+
ToolTipService.ShowDuration="3000"
52+
ToolTipService.BetweenShowDelay="100" >
53+
<Path Data="{StaticResource DeleteAlert}" Fill="Red"
54+
Width="16" Height="16" Stretch="Uniform" />
55+
</Button>
4756
</Grid>
4857

4958
<Separator Margin="0,40,0,40"/>
@@ -72,13 +81,22 @@
7281
<ColumnDefinition Width="*" />
7382
<ColumnDefinition Width="*" />
7483
<ColumnDefinition Width="*" />
84+
<ColumnDefinition Width="Auto" />
7585
</Grid.ColumnDefinitions>
7686
<Button Content="{Binding IsAddingCategory, Converter={StaticResource BoolToAddCancelConverter}}"
7787
Command="{Binding ToggleAddCategoryCommand}" Grid.Column="0" Margin="0,0,10,0" />
7888

7989
<Button Content="Save" Command="{Binding SaveCategoryCommand}" IsEnabled="{Binding SaveCategoryCommand.CanExecute}" Grid.Column="1" Margin="0,0,10,0"/>
80-
<Button Content="Delete Category" Grid.Column="2"
90+
<Button Content="Delete Category" Grid.Column="2" Margin="0,0,10,0"
8191
Command="{Binding DeleteCategoryCommand}" />
92+
<Button Grid.Column="3" Command="{Binding ForceDeleteCategoryCommand}"
93+
ToolTip="Deletes the category and ALL its snippets"
94+
ToolTipService.InitialShowDelay="300"
95+
ToolTipService.ShowDuration="3000"
96+
ToolTipService.BetweenShowDelay="100" >
97+
<Path Data="{StaticResource DeleteAlert}" Fill="Red"
98+
Width="16" Height="16" Stretch="Uniform" />
99+
</Button>
82100
</Grid>
83101
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,30,0,0">
84102
<Button Content="Close" Width="80" Margin="0,0,10,0" Command="{Binding CloseCommand}" />

src/CodeSnip/Views/LanguageCategoryView/LanguageCategoryViewModel.cs

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ partial void OnSelectedLanguageChanged(Language? value)
228228
{
229229
NewLanguageCode = value.Code ?? "";
230230
NewLanguageName = value.Name ?? "";
231+
SelectedLanguageForCategory = value;
231232
}
232233
}
233234

@@ -402,12 +403,11 @@ private async Task DeleteLanguageAsync()
402403
return;
403404

404405
_databaseService.DeleteLanguage(SelectedLanguage.Id);
405-
Languages.Remove(SelectedLanguage);
406-
SelectedLanguage = Languages.FirstOrDefault();
406+
HandleLanguageDeletion(SelectedLanguage);
407407
}
408408
catch (Exception ex)
409409
{
410-
await DialogService.Instance.ShowMessageAsync("Error", ex.Message);
410+
await DialogService.Instance.ShowMessageAsync("Error", $"Failed to delete language '{SelectedLanguage?.Name}'.\n\nDetails: {ex.Message}");
411411
}
412412
}
413413

@@ -426,8 +426,7 @@ private async Task DeleteCategoryAsync()
426426
return;
427427

428428
_databaseService.DeleteCategory(SelectedCategory.Id);
429-
SelectedLanguageForCategory.Categories.Remove(SelectedCategory);
430-
SelectedCategory = FilteredCategories.FirstOrDefault();
429+
HandleCategoryDeletion(SelectedCategory);
431430

432431
}
433432
catch (Exception ex)
@@ -438,11 +437,110 @@ private async Task DeleteCategoryAsync()
438437
}
439438
else
440439
{
441-
await DialogService.Instance.ShowMessageAsync("Error", ex.Message);
440+
await DialogService.Instance.ShowMessageAsync("Error", $"Failed to delete category '{SelectedCategory?.Name}'.\n\nDetails: {ex.Message}");
442441
}
443442
}
444443
}
445444

445+
[RelayCommand]
446+
private async Task ForceDeleteLanguage()
447+
{
448+
if (SelectedLanguage == null)
449+
{
450+
await DialogService.Instance.ShowMessageAsync("Action Skipped", "Please select a language to delete.");
451+
return;
452+
}
453+
454+
int snippetCount = _databaseService.CountSnippetsInLanguage(SelectedLanguage.Id);
455+
string message = $"Are you sure you want to permanently delete the language '{SelectedLanguage.Name}'?";
456+
457+
if (snippetCount > 0)
458+
{
459+
message += $"\n\nThis will also delete all of its categories and {snippetCount} associated snippets. This action cannot be undone.";
460+
}
461+
else
462+
{
463+
message += "\n\nThis will also delete all of its (empty) categories. This action cannot be undone.";
464+
}
465+
466+
bool confirm = await DialogService.Instance.ShowConfirmAsync("Force Delete Confirmation", message);
467+
468+
if (!confirm)
469+
return;
470+
471+
try
472+
{
473+
_databaseService.ForceDeleteLanguage(SelectedLanguage.Id);
474+
HandleLanguageDeletion(SelectedLanguage);
475+
}
476+
catch (Exception ex)
477+
{
478+
await DialogService.Instance.ShowMessageAsync("Error", $"Failed to delete language '{SelectedLanguage?.Name}'.\n\nDetails: {ex.Message}");
479+
}
480+
}
481+
482+
[RelayCommand]
483+
private async Task ForceDeleteCategory()
484+
{
485+
if (SelectedCategory == null || SelectedLanguageForCategory == null)
486+
{
487+
await DialogService.Instance.ShowMessageAsync("Action Skipped", "Please select a category to delete.");
488+
return;
489+
}
490+
491+
int snippetCount = _databaseService.CountSnippetsInCategory(SelectedCategory.Id);
492+
string message = $"Are you sure you want to permanently delete the category '{SelectedCategory.Name}'?";
493+
494+
if (snippetCount > 0)
495+
{
496+
message += $"\n\nThis will also delete {snippetCount} associated snippets. This action cannot be undone.";
497+
}
498+
else
499+
{
500+
message += "\n\nThis action cannot be undone.";
501+
}
502+
503+
bool confirm = await DialogService.Instance.ShowConfirmAsync("Force Delete Confirmation", message);
504+
505+
if (!confirm)
506+
return;
507+
508+
try
509+
{
510+
_databaseService.ForceDeleteCategory(SelectedCategory.Id);
511+
HandleCategoryDeletion(SelectedCategory);
512+
}
513+
catch (Exception ex)
514+
{
515+
await DialogService.Instance.ShowMessageAsync("Error", $"Failed to delete category '{SelectedCategory?.Name}'.\n\nDetails: {ex.Message}");
516+
}
517+
}
518+
519+
private void HandleLanguageDeletion(Language languageToDelete)
520+
{
521+
if (languageToDelete == null) return;
522+
523+
Languages.Remove(languageToDelete);
524+
SelectedLanguage = Languages.FirstOrDefault();
525+
526+
if (SelectedLanguage == null)
527+
{
528+
NewLanguageCode = string.Empty;
529+
NewLanguageName = string.Empty;
530+
SelectedLanguageForCategory = null;
531+
}
532+
}
533+
534+
private void HandleCategoryDeletion(Category categoryToDelete)
535+
{
536+
SelectedLanguageForCategory?.Categories.Remove(categoryToDelete);
537+
SelectedCategory = FilteredCategories.FirstOrDefault();
538+
if (SelectedCategory == null)
539+
{
540+
NewCategoryName = string.Empty;
541+
}
542+
}
543+
446544
[RelayCommand]
447545
private void Close()
448546
{

0 commit comments

Comments
 (0)