Skip to content

Add dashboard cruds and widgets#83

Merged
PhantomDave merged 8 commits intomainfrom
add-dashboard-cruds-and-widgets
Nov 24, 2025
Merged

Add dashboard cruds and widgets#83
PhantomDave merged 8 commits intomainfrom
add-dashboard-cruds-and-widgets

Conversation

@PhantomDave
Copy link
Copy Markdown
Owner

No description provided.

- Introduced CreateDashboardInput and UpdateDashboardInput classes for dashboard creation and updates.
- Added UpdateWidgetInput class to facilitate widget updates.
- Implemented DashboardMutations for creating, updating, and deleting dashboards.
- Created DashboardWidgetMutations for adding, updating, and removing widgets from dashboards.
- Developed DashboardType and DashboardWidgetType object types for GraphQL responses.
- Implemented DashboardQueries to retrieve dashboards and their associated widgets.
- Updated BankTrackerDbContext to include DbSet for Dashboards and DashboardWidgets.
- Added migrations to create Dashboards and DashboardWidgets tables with appropriate relationships.
- Enhanced BankImportTemplate and ImportResultType to use array initialization for dictionaries.
- Created Dashboard and DashboardWidget models with necessary properties and relationships.
PhantomDave and others added 3 commits November 24, 2025 11:00
…, function or class

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: PhantomDave <34485699+PhantomDave@users.noreply.github.com>
@PhantomDave PhantomDave marked this pull request as ready for review November 24, 2025 10:56
Copilot AI review requested due to automatic review settings November 24, 2025 10:56
public void GetAccountIdFromContext_WithNullHttpContext_ThrowsGraphQLException()
{
// Arrange
_mockHttpContextAccessor.Setup(a => a.HttpContext).Returns((HttpContext?)null);

Check warning

Code scanning / CodeQL

Useless upcast Warning

There is no need to upcast from
null
to
HttpContext
- the conversion can be done implicitly.

Copilot Autofix

AI 5 months ago

To fix the problem, simply remove the redundant explicit cast (HttpContext?) from the line where null is being returned or passed. In this precise case, within file PhantomDave.BankTracking.UnitTests/HttpContextExtensionsTests.cs on line 65, the call:

_mockHttpContextAccessor.Setup(a => a.HttpContext).Returns((HttpContext?)null);

should be changed to:

_mockHttpContextAccessor.Setup(a => a.HttpContext).Returns((HttpContext)null);

or — even better — since the method expects a nullable reference, it is perfectly correct and idiomatic to simply write:

_mockHttpContextAccessor.Setup(a => a.HttpContext).Returns(null);

no cast needed. No other code changes, imports, or definitions are needed.

Suggested changeset 1
PhantomDave.BankTracking.UnitTests/HttpContextExtensionsTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/PhantomDave.BankTracking.UnitTests/HttpContextExtensionsTests.cs b/PhantomDave.BankTracking.UnitTests/HttpContextExtensionsTests.cs
--- a/PhantomDave.BankTracking.UnitTests/HttpContextExtensionsTests.cs
+++ b/PhantomDave.BankTracking.UnitTests/HttpContextExtensionsTests.cs
@@ -62,7 +62,7 @@
     public void GetAccountIdFromContext_WithNullHttpContext_ThrowsGraphQLException()
     {
         // Arrange
-        _mockHttpContextAccessor.Setup(a => a.HttpContext).Returns((HttpContext?)null);
+        _mockHttpContextAccessor.Setup(a => a.HttpContext).Returns(null);
 
         // Act & Assert
         var exception = Assert.Throws<GraphQLException>(() => 
EOF
@@ -62,7 +62,7 @@
public void GetAccountIdFromContext_WithNullHttpContext_ThrowsGraphQLException()
{
// Arrange
_mockHttpContextAccessor.Setup(a => a.HttpContext).Returns((HttpContext?)null);
_mockHttpContextAccessor.Setup(a => a.HttpContext).Returns(null);

// Act & Assert
var exception = Assert.Throws<GraphQLException>(() =>
Copilot is powered by AI and may make mistakes. Always verify output.
public void GetAccountIdFromContext_WithNullUser_ThrowsGraphQLException()
{
// Arrange
_mockHttpContext.Setup(c => c.User).Returns((ClaimsPrincipal)null!);

Check warning

Code scanning / CodeQL

Useless upcast Warning

There is no need to upcast from
null
to
ClaimsPrincipal
- the conversion can be done implicitly.

Copilot Autofix

AI 5 months ago

To fix the issue, simply remove the explicit upcast (ClaimsPrincipal)null! to just null! (or even null if suppression of nullable reference warnings isn't needed) in the .Returns call on line 78. This change does not alter runtime behavior and the intent remains clear—configuring the mock to return null for a property of type ClaimsPrincipal. Only the unnecessary explicit cast is removed; no other code changes are required.

Suggested changeset 1
PhantomDave.BankTracking.UnitTests/HttpContextExtensionsTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/PhantomDave.BankTracking.UnitTests/HttpContextExtensionsTests.cs b/PhantomDave.BankTracking.UnitTests/HttpContextExtensionsTests.cs
--- a/PhantomDave.BankTracking.UnitTests/HttpContextExtensionsTests.cs
+++ b/PhantomDave.BankTracking.UnitTests/HttpContextExtensionsTests.cs
@@ -75,7 +75,7 @@
     public void GetAccountIdFromContext_WithNullUser_ThrowsGraphQLException()
     {
         // Arrange
-        _mockHttpContext.Setup(c => c.User).Returns((ClaimsPrincipal)null!);
+        _mockHttpContext.Setup(c => c.User).Returns(null!);
         _mockHttpContextAccessor.Setup(a => a.HttpContext).Returns(_mockHttpContext.Object);
 
         // Act & Assert
EOF
@@ -75,7 +75,7 @@
public void GetAccountIdFromContext_WithNullUser_ThrowsGraphQLException()
{
// Arrange
_mockHttpContext.Setup(c => c.User).Returns((ClaimsPrincipal)null!);
_mockHttpContext.Setup(c => c.User).Returns(null!);
_mockHttpContextAccessor.Setup(a => a.HttpContext).Returns(_mockHttpContext.Object);

// Act & Assert
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds comprehensive dashboard functionality to the BankTracker application, including CRUD operations for dashboards and widgets on both backend and frontend, with full unit test coverage.

Purpose: Enable users to create customizable dashboards with draggable/resizable widgets to visualize financial data (current balance, net income graphs).

Key Changes:

  • Backend: Added Dashboard and DashboardWidget entities with GraphQL queries/mutations, service layer, EF Core migrations, and 19 comprehensive unit tests
  • Frontend: Refactored widget components to use signals and shared service state, integrated angular-gridster2 for drag-and-drop dashboard layout, added NetGraph widget for yearly trends
  • Architecture: Widgets now fetch their own data independently rather than receiving it via inputs

Reviewed changes

Copilot reviewed 45 out of 48 changed files in this pull request and generated 21 comments.

Show a summary per file
File Description
PhantomDave.BankTracking.Library/Models/Dashboard.cs New Dashboard entity with AccountId and widget collection
PhantomDave.BankTracking.Library/Models/DashboardWidget.cs New DashboardWidget entity with position/size and WidgetType enum
PhantomDave.BankTracking.Data/Context/BankTrackerDbContext.cs EF Core configuration for Dashboard and DashboardWidget entities
PhantomDave.BankTracking.Data/Migrations/* Database migrations for dashboard tables
PhantomDave.BankTracking.Data/UnitOfWork/IUnitOfWork.cs Added Dashboard and DashboardWidget repositories
PhantomDave.BankTracking.Api/Services/DashboardService.cs Service layer with validation, normalization, and CRUD logic for dashboards/widgets
PhantomDave.BankTracking.Api/Types/Queries/DashboardQueries.cs GraphQL queries with authorization for fetching dashboards
PhantomDave.BankTracking.Api/Types/Mutations/Dashboard*.cs GraphQL mutations for dashboard and widget CRUD (missing [Authorize] attributes)
PhantomDave.BankTracking.Api/Types/ObjectTypes/Dashboard*.cs GraphQL types for dashboard responses
PhantomDave.BankTracking.Api/Types/Inputs/*.cs Input types for dashboard/widget mutations
PhantomDave.BankTracking.UnitTests/Services/DashboardServiceTests.cs 19 comprehensive unit tests covering validation, normalization, and edge cases
frontend/src/app/widgets/widget-remaining/* Refactored to inject FinanceRecordService and fetch own data
frontend/src/app/widgets/widget-net-graph/* New widget displaying yearly net balance trend chart
frontend/src/app/widgets/widget-wrapper/* Added edit mode support with drag handle
frontend/src/app/components/dashboard-component/* Dashboard with gridster integration and edit mode toggle
frontend/src/app/models/dashboards/gridster-item.ts Widget interface extending GridsterItem with WidgetType enum
TESTING.md Documentation of 19 new DashboardService unit tests
Files not reviewed (2)
  • PhantomDave.BankTracking.Data/Migrations/20251124092813_Added Dashboard and Widgets.Designer.cs: Language not supported
  • PhantomDave.BankTracking.Data/Migrations/20251124094829_AddAccountIdToDashboard.Designer.cs: Language not supported

Comment on lines +20 to +24
var dashboard = new Dashboard
{
AccountId = accountId,
Name = input.Name?.Trim() ?? string.Empty
};
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing name truncation: The CreateDashboard mutation doesn't enforce the 100-character name length limit that's defined in the database configuration (ConfigureDashboard in BankTrackerDbContext.cs line 115) and enforced in DashboardService.NormalizeName. Consider truncating the name to 100 characters before saving to prevent potential database errors.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

check also the security warning from codeQL

{
dashboard.Name = input.Name.Trim();
}

Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing UpdateAsync call: The mutation modifies the dashboard entity but only calls SaveChangesAsync() without calling unitOfWork.Dashboards.UpdateAsync(dashboard). While EF Core change tracking may handle this, it's inconsistent with the pattern used in DashboardService.UpdateDashboardAsync (line 86) and other mutations. Consider calling UpdateAsync for consistency and explicit intent.

Suggested change
await unitOfWork.Dashboards.UpdateAsync(dashboard);

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +57
public async Task<DashboardType> UpdateDashboard(
[Service] IUnitOfWork unitOfWork,
[Service] IHttpContextAccessor httpContextAccessor,
UpdateDashboardInput input)
{
var accountId = httpContextAccessor.GetAccountIdFromContext();

var dashboard = await unitOfWork.Dashboards.GetByIdAsync(input.Id);
if (dashboard == null || dashboard.AccountId != accountId)
{
throw new GraphQLException(
ErrorBuilder.New()
.SetMessage("Dashboard not found.")
.SetCode("NOT_FOUND")
.Build());
}

if (input.Name != null)
{
dashboard.Name = input.Name.Trim();
}

await unitOfWork.SaveChangesAsync();

return DashboardType.FromDashboard(dashboard);
}
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing [Authorize] attribute: This mutation allows updating dashboards without authentication. Add the [Authorize] attribute to ensure only authenticated users can update dashboards, consistent with the query patterns in DashboardQueries.cs.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +23
var dashboard = new Dashboard
{
AccountId = accountId,
Name = input.Name?.Trim() ?? string.Empty
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input validation: The CreateDashboard mutation allows empty dashboard names (after trimming whitespace becomes an empty string). According to DashboardService.CreateDashboardAsync, this should return null, but here it proceeds to create the dashboard. Consider validating that the trimmed name is not empty and throw a BAD_USER_INPUT error if it is, consistent with other mutations.

Suggested change
var dashboard = new Dashboard
{
AccountId = accountId,
Name = input.Name?.Trim() ?? string.Empty
var trimmedName = input.Name?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(trimmedName))
{
throw new GraphQLException(
ErrorBuilder.New()
.SetMessage("Dashboard name cannot be empty.")
.SetCode("BAD_USER_INPUT")
.Build());
}
var dashboard = new Dashboard
{
AccountId = accountId,
Name = trimmedName

Copilot uses AI. Check for mistakes.

if (input.Name != null)
{
dashboard.Name = input.Name.Trim();
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing name truncation: The UpdateDashboard mutation doesn't truncate the name to 100 characters like DashboardService.NormalizeName does. Consider truncating the name before saving to maintain consistency with the service layer and prevent potential database errors.

Suggested change
dashboard.Name = input.Name.Trim();
dashboard.Name = input.Name.Trim().Length > 100
? input.Name.Trim().Substring(0, 100)
: input.Name.Trim();

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +80
public async Task<bool> DeleteDashboard(
[Service] IUnitOfWork unitOfWork,
[Service] IHttpContextAccessor httpContextAccessor,
int id)
{
var accountId = httpContextAccessor.GetAccountIdFromContext();

var dashboard = await unitOfWork.Dashboards.GetByIdAsync(id);
if (dashboard == null || dashboard.AccountId != accountId)
{
throw new GraphQLException(
ErrorBuilder.New()
.SetMessage("Dashboard not found.")
.SetCode("NOT_FOUND")
.Build());
}

await unitOfWork.Dashboards.DeleteAsync(id);
await unitOfWork.SaveChangesAsync();

return true;
}
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing [Authorize] attribute: This mutation allows deleting dashboards without authentication. Add the [Authorize] attribute to ensure only authenticated users can delete dashboards, consistent with the query patterns in DashboardQueries.cs.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +53
public async Task<DashboardWidgetType> AddWidget(
[Service] IUnitOfWork unitOfWork,
[Service] IHttpContextAccessor httpContextAccessor,
AddWidgetInput input)
{
var accountId = httpContextAccessor.GetAccountIdFromContext();

var dashboard = await unitOfWork.Dashboards.GetByIdAsync(input.DashboardId);
if (dashboard is null || dashboard.AccountId != accountId)
{
throw new GraphQLException(
ErrorBuilder.New()
.SetMessage("Dashboard not found.")
.SetCode("NOT_FOUND")
.Build());
}

if (input.Rows <= 0 || input.Cols <= 0)
{
throw new GraphQLException(
ErrorBuilder.New()
.SetMessage("Widget rows and cols must be greater than 0.")
.SetCode("BAD_USER_INPUT")
.Build());
}

var widget = new DashboardWidget
{
DashboardId = input.DashboardId,
Type = input.Type,
X = Math.Max(0, input.X),
Y = Math.Max(0, input.Y),
Rows = input.Rows,
Cols = input.Cols
};

await unitOfWork.DashboardWidgets.AddAsync(widget);
await unitOfWork.SaveChangesAsync();

return DashboardWidgetType.FromDashboardWidget(widget);
}
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing [Authorize] attribute: This mutation allows adding widgets without authentication. Add the [Authorize] attribute to ensure only authenticated users can add widgets, consistent with the query patterns in DashboardQueries.cs.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +129
public async Task<DashboardWidgetType> UpdateWidget(
[Service] IUnitOfWork unitOfWork,
[Service] IHttpContextAccessor httpContextAccessor,
UpdateWidgetInput input)
{
var accountId = httpContextAccessor.GetAccountIdFromContext();

var widget = await unitOfWork.DashboardWidgets.GetByIdAsync(input.Id);
if (widget is null)
{
throw new GraphQLException(
ErrorBuilder.New()
.SetMessage("Widget not found.")
.SetCode("NOT_FOUND")
.Build());
}

var dashboard = await unitOfWork.Dashboards.GetByIdAsync(widget.DashboardId);
if (dashboard is null || dashboard.AccountId != accountId)
{
throw new GraphQLException(
ErrorBuilder.New()
.SetMessage("Widget not found.")
.SetCode("NOT_FOUND")
.Build());
}

if (input.Type.HasValue)
{
widget.Type = input.Type.Value;
}

if (input.X.HasValue)
{
widget.X = Math.Max(0, input.X.Value);
}

if (input.Y.HasValue)
{
widget.Y = Math.Max(0, input.Y.Value);
}

if (input.Rows.HasValue)
{
if (input.Rows.Value <= 0)
{
throw new GraphQLException(
ErrorBuilder.New()
.SetMessage("Widget rows must be greater than 0.")
.SetCode("BAD_USER_INPUT")
.Build());
}

widget.Rows = input.Rows.Value;
}

if (input.Cols.HasValue)
{
if (input.Cols.Value <= 0)
{
throw new GraphQLException(
ErrorBuilder.New()
.SetMessage("Widget cols must be greater than 0.")
.SetCode("BAD_USER_INPUT")
.Build());
}

widget.Cols = input.Cols.Value;
}

await unitOfWork.DashboardWidgets.UpdateAsync(widget);
await unitOfWork.SaveChangesAsync();

return DashboardWidgetType.FromDashboardWidget(widget);
}
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing [Authorize] attribute: This mutation allows updating widgets without authentication. Add the [Authorize] attribute to ensure only authenticated users can update widgets, consistent with the query patterns in DashboardQueries.cs.

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +162
public async Task<bool> RemoveWidget(
[Service] IUnitOfWork unitOfWork,
[Service] IHttpContextAccessor httpContextAccessor,
int id)
{
var accountId = httpContextAccessor.GetAccountIdFromContext();

var widget = await unitOfWork.DashboardWidgets.GetByIdAsync(id);
if (widget is null)
{
throw new GraphQLException(
ErrorBuilder.New()
.SetMessage("Widget not found.")
.SetCode("NOT_FOUND")
.Build());
}

var dashboard = await unitOfWork.Dashboards.GetByIdAsync(widget.DashboardId);
if (dashboard is null || dashboard.AccountId != accountId)
{
throw new GraphQLException(
ErrorBuilder.New()
.SetMessage("Widget not found.")
.SetCode("NOT_FOUND")
.Build());
}

await unitOfWork.DashboardWidgets.DeleteAsync(id);
await unitOfWork.SaveChangesAsync();

return true;
}
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing [Authorize] attribute: This mutation allows removing widgets without authentication. Add the [Authorize] attribute to ensure only authenticated users can remove widgets, consistent with the query patterns in DashboardQueries.cs.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,27 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import DebugElement.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI commented Nov 24, 2025

@PhantomDave I've opened a new pull request, #92, to work on those changes. Once the pull request is ready, I'll request review from you.

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: PhantomDave <34485699+PhantomDave@users.noreply.github.com>
@PhantomDave
Copy link
Copy Markdown
Owner Author

@copilot open a new pull request to apply changes based on the comments in this thread

Also implement everything under the codeQL warning

Copy link
Copy Markdown
Contributor

Copilot AI commented Nov 24, 2025

@PhantomDave I've opened a new pull request, #93, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 2 commits November 24, 2025 12:31
… APIs (#93)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: PhantomDave <34485699+PhantomDave@users.noreply.github.com>
@PhantomDave PhantomDave merged commit 82e5159 into main Nov 24, 2025
14 checks passed
@PhantomDave PhantomDave deleted the add-dashboard-cruds-and-widgets branch November 24, 2025 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants