Added basic search implementation with display components

This commit is contained in:
Vincent Allen
2025-10-16 09:24:52 -04:00
852 changed files with 6519 additions and 29467 deletions

View File

@@ -35,6 +35,7 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
.ThenInclude(e => e.ListedNames)
.Include(g => g.ChildArtifactEntries)
.ThenInclude(e => e.Defects)
.Where(g => g.Id == id)
.FirstOrDefaultAsync();
}
@@ -110,7 +111,7 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
// Check if the type exists in the database.
var existingType = await GetUniqueTypeAsync(entry.Type);
entry.Type = existingType;
// Handle Storage Location
// Check if the storage location exists in the database.
var existingLocation = await context.ArtifactStorageLocations.FirstOrDefaultAsync(l => l.Location == entry.StorageLocation.Location);
@@ -187,18 +188,27 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
var managedFilePaths = new List<FilePathListing>();
foreach (var filepath in entry.Files)
{
// Attempt to find the file path in the database.
var existingFilePath = await context.ArtifactFilePaths.FirstOrDefaultAsync(f => f.Path == filepath.Path);
if (existingFilePath != null)
{
// The file path already exists. Use the tracked instance.
managedFilePaths.Add(existingFilePath);
}
else
{
// The file path is new. Add it to the managed list.
managedFilePaths.Add(filepath);
}
}
// Replace the disconnected file path objects on the entry with the managed ones.
entry.Files = managedFilePaths;
}
// Concatinate all of the text to be searchable by postgres
grouping.GenerateSearchIndex();
// Add the new grouping and save changes.
//context.ArtifactGroupings.Add(grouping);
@@ -219,138 +229,6 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
await context.SaveChangesAsync();
}
/*
public async Task UpdateGroupingAsync(ArtifactGrouping updatedGrouping)
{
// The DbContext is provided externally, so we will use it as is.
// Assuming you have an instance available, e.g., via a constructor or method parameter.
await using var context = await _context.CreateDbContextAsync();
// 1. Retrieve the existing grouping object from the database, eagerly loading all related data.
// This is crucial for correctly handling all relationships.
var existingGrouping = await context.ArtifactGroupings
.Include(g => g.Category)
.Include(g => g.IdentifierFields)
.Include(g => g.Type)
.Include(g => g.ChildArtifactEntries)
.ThenInclude(e => e.StorageLocation)
.Include(g => g.ChildArtifactEntries)
.ThenInclude(e => e.Type)
.Include(g => g.ChildArtifactEntries)
.ThenInclude(e => e.Files)
.Include(g => g.ChildArtifactEntries)
.ThenInclude(e => e.Tags)
.Include(g => g.ChildArtifactEntries)
.ThenInclude(e => e.ListedNames)
.Include(g => g.ChildArtifactEntries)
.ThenInclude(e => e.Defects)
.Where(g => g.Id == updatedGrouping.Id)
.FirstOrDefaultAsync();
if (existingGrouping == null)
{
// The grouping does not exist. You may want to throw an exception or handle this case.
return;
}
// 2. Manually copy over primitive properties.
existingGrouping.Title = updatedGrouping.Title;
existingGrouping.Description = updatedGrouping.Description;
existingGrouping.IsPublicallyVisible = updatedGrouping.IsPublicallyVisible;
existingGrouping.IdentifierFields = updatedGrouping.IdentifierFields;
// Handle one-to-many relationships (Type, Category).
// Find the existing related entity and attach it to the tracked graph.
var existingGroupingType = await context.ArtifactTypes.FirstOrDefaultAsync(t => t.Name == updatedGrouping.Type.Name);
if (existingGroupingType != null)
{
existingGrouping.Type = existingGroupingType;
}
else
{
existingGrouping.Type = updatedGrouping.Type;
}
// Attach the category as specified
if (existingGrouping.Category.Name != updatedGrouping.Category.Name)
{
existingGrouping.Category = updatedGrouping.Category;
context.Add(existingGrouping.Category);
}
// 3. Synchronize the ChildArtifactEntries collection.
// First, remove any entries that were deleted in the DTO.
var updatedEntryIds = updatedGrouping.ChildArtifactEntries.Select(e => e.Id).ToList();
var entriesToRemove = existingGrouping.ChildArtifactEntries
.Where(e => !updatedEntryIds.Contains(e.Id))
.ToList();
foreach (var entryToRemove in entriesToRemove)
{
existingGrouping.ChildArtifactEntries.Remove(entryToRemove);
}
// Now, loop through the updated entries to handle updates and additions.
foreach (var updatedEntry in updatedGrouping.ChildArtifactEntries)
{
var existingEntry = existingGrouping.ChildArtifactEntries
.FirstOrDefault(e => e.Id == updatedEntry.Id);
if (existingEntry != null)
{
// The entry exists, so manually update its properties.
existingEntry.Title = updatedEntry.Title;
existingEntry.Description = updatedEntry.Description;
existingEntry.ArtifactNumber = updatedEntry.ArtifactNumber;
existingEntry.IsPubliclyVisible = updatedEntry.IsPubliclyVisible;
existingEntry.AssociatedDates = updatedEntry.AssociatedDates;
existingEntry.FileTextContent = updatedEntry.FileTextContent;
// Handle one-to-many relationships (StorageLocation, Type)
var existingLocation = await context.ArtifactStorageLocations.FirstOrDefaultAsync(l => l.Location == updatedEntry.StorageLocation.Location);
if (existingLocation != null)
{
existingEntry.StorageLocation = existingLocation;
}
else
{
existingEntry.StorageLocation = updatedEntry.StorageLocation;
context.Add(existingEntry.StorageLocation);
}
var existingType = await context.ArtifactTypes.FirstOrDefaultAsync(t => t.Name == updatedEntry.Type.Name);
if (existingType != null)
{
existingEntry.Type = existingType;
}
else
{
existingEntry.Type = updatedEntry.Type;
context.Add(existingEntry.Type);
}
// Synchronize many-to-many collections.
// This is the most complex part. We need to handle adds and removals.
// Helper function to handle a generic collection sync.
await SyncCollectionAsync<ArtifactEntryTag, string>(context, existingEntry.Tags, updatedEntry.Tags, t => t.Name);
await SyncCollectionAsync<ListedName, string>(context, existingEntry.ListedNames, updatedEntry.ListedNames, n => n.Value);
await SyncCollectionAsync<ArtifactDefect, string>(context, existingEntry.Defects, updatedEntry.Defects, d => d.Description);
}
else
{
// The entry is new, so add it to the tracked collection.
existingGrouping.ChildArtifactEntries.Add(updatedEntry);
}
}
// 4. Save all changes.
await context.SaveChangesAsync();
}
*/
public async Task UpdateGroupingAsync(ArtifactGrouping updatedGrouping)
{
// The DbContext is provided externally, so we will use it as is.
@@ -409,6 +287,10 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
context.Add(existingGrouping.Category);
}
// Update top-level properties.
existingGrouping.Title = updatedGrouping.Title;
existingGrouping.IsPublicallyVisible = updatedGrouping.IsPublicallyVisible;
existingGrouping.Description = updatedGrouping.Description;
// 3. Synchronize the ChildArtifactEntries collection.
// First, remove any entries that were deleted in the DTO.
@@ -461,10 +343,11 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
{
// The entry is new and its children are already de-duplicated, so just add it.
existingGrouping.ChildArtifactEntries.Add(updatedEntry);
}
}
existingGrouping.GenerateSearchIndex();
// 4. Save all changes.
await context.SaveChangesAsync();
}
@@ -498,14 +381,14 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
// De-duplicate ListedNames
var processedNames = new List<ListedName>();
if (entry.ListedNames != null)
{
{
foreach (var name in entry.ListedNames)
{
{
var existingName = await context.ArtifactAssociatedNames.FirstOrDefaultAsync(n => n.Value == name.Value) ?? name;
processedNames.Add(existingName);
}
entry.ListedNames = processedNames;
}
}
// De-duplicate Defects
var processedDefects = new List<ArtifactDefect>();
@@ -515,19 +398,19 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
{
var existingDefect = await context.ArtifactDefects.FirstOrDefaultAsync(d => d.Description == defect.Description) ?? defect;
processedDefects.Add(existingDefect);
}
}
entry.Defects = processedDefects;
}
}
if (entry.Files.Any())
{
{
// 1. Get the IDs from the incoming, untracked file objects.
var inputFileIds = entry.Files.Select(f => f.Id).ToList();
// 2. Fetch the actual, tracked entities from the database.
var trackedFiles = await context.ArtifactFilePaths
.Where(dbFile => inputFileIds.Contains(dbFile.Id))
.ToListAsync();
.ToListAsync();
// 3. Replace the untracked collection with the tracked one.
entry.Files = trackedFiles;
@@ -557,9 +440,9 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
// 2. Identify keys for brand new items
var keysToAdd = updatedKeys.Except(existingKeys).ToList();
if (!keysToAdd.Any())
{
{
return; // Nothing to add
}
}
// 3. Batch-fetch all entities from the DB that match the new keys.
// This is the key change to make the query translatable to SQL.
@@ -579,7 +462,7 @@ public class ArtifactGroupingProvider : IArtifactGroupingProvider
.Where(n => nameKeys.Contains(n.Value))
.ToListAsync();
existingDbItemsMap = names.ToDictionary(n => (TKey)(object)n.Value) as Dictionary<TKey, TEntity>;
}
}
else if (typeof(TEntity) == typeof(ArtifactDefect))
{
var defectKeys = keysToAdd.Cast<string>().ToList();