mirror of https://github.com/ghostfolio/ghostfolio
22 changed files with 1771 additions and 523 deletions
@ -0,0 +1,547 @@ |
|||||
|
# Budget UI Repair Implementation Plan |
||||
|
|
||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
||||
|
|
||||
|
**Goal:** Fix the broken Budget page category management UI so the dialog is readable, actions render as compact icons, and the layout works in dark mode at desktop and narrow widths. |
||||
|
|
||||
|
**Architecture:** Keep the existing Angular Material dialog/table pattern, but stop relying on Material icon font ligatures for action buttons in the Budget feature. Use Ghostfolio's existing Ionic icon pattern for edit/delete actions, constrain action columns, and make the category form/table responsive with stable widths. |
||||
|
|
||||
|
**Tech Stack:** Angular 21 standalone components, Angular Material dialog/table/form controls, Ionic standalone icons, Jest, Nx. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Current Breakage From Screenshots |
||||
|
|
||||
|
- In `Manage categories`, action icons render as visible text (`edit delete`) and overflow the table row. |
||||
|
- The dialog content is too narrow for the form plus action column, so controls feel cramped and clipped. |
||||
|
- The category table action column has no stable width, so icon/text fallback can steal space from data columns. |
||||
|
- The Budget page uses the same `mat-icon` pattern for budget row edit/delete actions, so it should be fixed in the same pass. |
||||
|
|
||||
|
## File Structure |
||||
|
|
||||
|
- Modify `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.ts`: replace `MatIconModule` dependency with `IonIcon`, register `createOutline` and `trashOutline`. |
||||
|
- Modify `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.html`: replace `<mat-icon>` action content with `<ion-icon>`, add action button classes, and improve table/action markup. |
||||
|
- Modify `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.scss`: add responsive dialog form layout, stable action column/button sizing, color swatch truncation, and mobile table behavior. |
||||
|
- Modify `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.spec.ts`: assert icon buttons exist by aria labels, form actions remain usable, and category color renders without exposing raw icon ligature text. |
||||
|
- Modify `apps/client/src/app/pages/portfolio/budget/budget-page.component.ts`: replace `MatIconModule` dependency with `IonIcon`, register `createOutline` and `trashOutline`, widen category dialog. |
||||
|
- Modify `apps/client/src/app/pages/portfolio/budget/budget-page.html`: replace budget row `<mat-icon>` actions with Ionic icons and stable action button classes. |
||||
|
- Modify `apps/client/src/app/pages/portfolio/budget/budget-page.scss`: add stable table action sizing and horizontal overflow handling for dense budget rows. |
||||
|
- Modify `apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts`: assert budget row actions render without raw icon words and dialog opens with the wider dimensions. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Task 1: Replace Broken Material Icon Ligatures In Category Dialog |
||||
|
|
||||
|
**Files:** |
||||
|
|
||||
|
- Modify: `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.ts` |
||||
|
- Modify: `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.html` |
||||
|
- Modify: `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.spec.ts` |
||||
|
|
||||
|
- [ ] **Step 1: Write the failing icon rendering test** |
||||
|
|
||||
|
Add this assertion to `it('loads expense categories', ...)` after `await fixture.whenStable();`: |
||||
|
|
||||
|
```ts |
||||
|
expect( |
||||
|
fixture.nativeElement.querySelector('[aria-label="Edit category"] ion-icon') |
||||
|
).not.toBeNull(); |
||||
|
expect( |
||||
|
fixture.nativeElement.querySelector('[aria-label="Delete category"] ion-icon') |
||||
|
).not.toBeNull(); |
||||
|
expect(fixture.nativeElement.textContent).not.toContain('editdelete'); |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 2: Run the category dialog spec and confirm it fails** |
||||
|
|
||||
|
Run: |
||||
|
|
||||
|
```bash |
||||
|
./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.spec.ts --runInBand --watchman=false |
||||
|
``` |
||||
|
|
||||
|
Expected: FAIL because the template still uses `<mat-icon>` instead of `<ion-icon>`. |
||||
|
|
||||
|
- [ ] **Step 3: Replace Material icons with Ionic icons in the component** |
||||
|
|
||||
|
In `manage-budget-categories-dialog.component.ts`, replace the `MatIconModule` import and component dependency with Ionic icons: |
||||
|
|
||||
|
```ts |
||||
|
import { IonIcon } from '@ionic/angular/standalone'; |
||||
|
import { addIcons } from 'ionicons'; |
||||
|
import { createOutline, trashOutline } from 'ionicons/icons'; |
||||
|
``` |
||||
|
|
||||
|
Remove: |
||||
|
|
||||
|
```ts |
||||
|
import { MatIconModule } from '@angular/material/icon'; |
||||
|
``` |
||||
|
|
||||
|
Update `imports`: |
||||
|
|
||||
|
```ts |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
IonIcon, |
||||
|
MatButtonModule, |
||||
|
MatDialogModule, |
||||
|
MatFormFieldModule, |
||||
|
MatInputModule, |
||||
|
MatTableModule, |
||||
|
ReactiveFormsModule |
||||
|
], |
||||
|
``` |
||||
|
|
||||
|
Add this constructor body after dependency injection parameters: |
||||
|
|
||||
|
```ts |
||||
|
) { |
||||
|
addIcons({ createOutline, trashOutline }); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 4: Replace the category action button markup** |
||||
|
|
||||
|
In `manage-budget-categories-dialog.html`, replace the two `<mat-icon>` button bodies with: |
||||
|
|
||||
|
```html |
||||
|
<button |
||||
|
class="category-action-button" |
||||
|
mat-icon-button |
||||
|
type="button" |
||||
|
[attr.aria-label]="'Edit category'" |
||||
|
(click)="onEditCategory(category)" |
||||
|
> |
||||
|
<ion-icon aria-hidden="true" name="create-outline" /> |
||||
|
</button> |
||||
|
<button |
||||
|
class="category-action-button" |
||||
|
mat-icon-button |
||||
|
type="button" |
||||
|
[attr.aria-label]="'Delete category'" |
||||
|
(click)="onDeleteCategory(category.id)" |
||||
|
> |
||||
|
<ion-icon aria-hidden="true" name="trash-outline" /> |
||||
|
</button> |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 5: Run the category dialog spec and confirm it passes** |
||||
|
|
||||
|
Run: |
||||
|
|
||||
|
```bash |
||||
|
./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.spec.ts --runInBand --watchman=false |
||||
|
``` |
||||
|
|
||||
|
Expected: PASS. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Task 2: Repair Category Dialog Layout And Responsiveness |
||||
|
|
||||
|
**Files:** |
||||
|
|
||||
|
- Modify: `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.html` |
||||
|
- Modify: `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.scss` |
||||
|
- Modify: `apps/client/src/app/pages/portfolio/budget/budget-page.component.ts` |
||||
|
|
||||
|
- [ ] **Step 1: Widen the dialog safely** |
||||
|
|
||||
|
In `budget-page.component.ts`, update `onManageCategories()`: |
||||
|
|
||||
|
```ts |
||||
|
this.dialog |
||||
|
.open(GfManageBudgetCategoriesDialogComponent, { |
||||
|
maxWidth: 'calc(100vw - 2rem)', |
||||
|
width: '42rem' |
||||
|
}) |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 2: Add stable table classes in the category template** |
||||
|
|
||||
|
In `manage-budget-categories-dialog.html`, update the table element: |
||||
|
|
||||
|
```html |
||||
|
<table |
||||
|
class="category-table gf-table w-100" |
||||
|
mat-table |
||||
|
[dataSource]="dataSource" |
||||
|
> |
||||
|
``` |
||||
|
|
||||
|
Update the actions column cells: |
||||
|
|
||||
|
```html |
||||
|
<th |
||||
|
*matHeaderCellDef |
||||
|
class="category-actions-column text-right" |
||||
|
mat-header-cell |
||||
|
></th> |
||||
|
<td |
||||
|
*matCellDef="let category" |
||||
|
class="category-actions-column text-right" |
||||
|
mat-cell |
||||
|
> |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 3: Add defensive layout CSS** |
||||
|
|
||||
|
Replace `manage-budget-categories-dialog.scss` with: |
||||
|
|
||||
|
```scss |
||||
|
:host { |
||||
|
display: block; |
||||
|
} |
||||
|
|
||||
|
.category-form { |
||||
|
align-items: start; |
||||
|
display: grid; |
||||
|
gap: 1rem; |
||||
|
grid-template-columns: minmax(12rem, 1fr) minmax(8rem, 10rem) auto; |
||||
|
} |
||||
|
|
||||
|
.category-form-actions { |
||||
|
align-items: center; |
||||
|
display: flex; |
||||
|
gap: 0.5rem; |
||||
|
justify-content: flex-end; |
||||
|
min-height: 3.5rem; |
||||
|
} |
||||
|
|
||||
|
.category-table { |
||||
|
table-layout: fixed; |
||||
|
} |
||||
|
|
||||
|
.category-table .mat-column-name { |
||||
|
width: 45%; |
||||
|
} |
||||
|
|
||||
|
.category-table .mat-column-color { |
||||
|
width: 35%; |
||||
|
} |
||||
|
|
||||
|
.category-actions-column { |
||||
|
white-space: nowrap; |
||||
|
width: 6rem; |
||||
|
} |
||||
|
|
||||
|
.category-action-button { |
||||
|
height: 2.25rem; |
||||
|
line-height: 2.25rem; |
||||
|
width: 2.25rem; |
||||
|
} |
||||
|
|
||||
|
.category-action-button ion-icon { |
||||
|
font-size: 1.125rem; |
||||
|
} |
||||
|
|
||||
|
.category-color { |
||||
|
align-items: center; |
||||
|
display: inline-flex; |
||||
|
gap: 0.5rem; |
||||
|
max-width: 100%; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.category-color-swatch { |
||||
|
border: 1px solid var(--border-color); |
||||
|
display: inline-block; |
||||
|
flex: 0 0 auto; |
||||
|
height: 1rem; |
||||
|
width: 1rem; |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 767.98px) { |
||||
|
.category-form { |
||||
|
grid-template-columns: 1fr; |
||||
|
} |
||||
|
|
||||
|
.category-form-actions { |
||||
|
justify-content: flex-start; |
||||
|
min-height: auto; |
||||
|
} |
||||
|
|
||||
|
.category-table { |
||||
|
min-width: 32rem; |
||||
|
} |
||||
|
|
||||
|
mat-dialog-content { |
||||
|
overflow-x: auto; |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 4: Update the budget page dialog width test** |
||||
|
|
||||
|
In `budget-page.component.spec.ts`, update the category dialog expectation to: |
||||
|
|
||||
|
```ts |
||||
|
expect(dialog.open).toHaveBeenCalledWith(expect.any(Function), { |
||||
|
maxWidth: 'calc(100vw - 2rem)', |
||||
|
width: '42rem' |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 5: Run the page and category specs** |
||||
|
|
||||
|
Run: |
||||
|
|
||||
|
```bash |
||||
|
./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.spec.ts --runInBand --watchman=false |
||||
|
./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts --runInBand --watchman=false |
||||
|
``` |
||||
|
|
||||
|
Expected: both PASS. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Task 3: Fix Budget Table Row Action Icons And Dense Table Overflow |
||||
|
|
||||
|
**Files:** |
||||
|
|
||||
|
- Modify: `apps/client/src/app/pages/portfolio/budget/budget-page.component.ts` |
||||
|
- Modify: `apps/client/src/app/pages/portfolio/budget/budget-page.html` |
||||
|
- Modify: `apps/client/src/app/pages/portfolio/budget/budget-page.scss` |
||||
|
- Modify: `apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts` |
||||
|
|
||||
|
- [ ] **Step 1: Add failing budget action icon assertions** |
||||
|
|
||||
|
In `budget-page.component.spec.ts`, inside `it('loads and renders budgets for the selected month', ...)`, add: |
||||
|
|
||||
|
```ts |
||||
|
expect( |
||||
|
fixture.nativeElement.querySelector('[aria-label="Edit budget"] ion-icon') |
||||
|
).not.toBeNull(); |
||||
|
expect( |
||||
|
fixture.nativeElement.querySelector('[aria-label="Delete budget"] ion-icon') |
||||
|
).not.toBeNull(); |
||||
|
expect(fixture.nativeElement.textContent).not.toContain('editdelete'); |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 2: Run the budget page spec and confirm it fails** |
||||
|
|
||||
|
Run: |
||||
|
|
||||
|
```bash |
||||
|
./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts --runInBand --watchman=false |
||||
|
``` |
||||
|
|
||||
|
Expected: FAIL because budget row actions still use `<mat-icon>`. |
||||
|
|
||||
|
- [ ] **Step 3: Replace Material icon dependency in the budget page** |
||||
|
|
||||
|
In `budget-page.component.ts`, add: |
||||
|
|
||||
|
```ts |
||||
|
import { IonIcon } from '@ionic/angular/standalone'; |
||||
|
import { addIcons } from 'ionicons'; |
||||
|
import { createOutline, trashOutline } from 'ionicons/icons'; |
||||
|
``` |
||||
|
|
||||
|
Remove: |
||||
|
|
||||
|
```ts |
||||
|
import { MatIconModule } from '@angular/material/icon'; |
||||
|
``` |
||||
|
|
||||
|
Update `imports`: |
||||
|
|
||||
|
```ts |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
GfValueComponent, |
||||
|
IonIcon, |
||||
|
MatButtonModule, |
||||
|
MatProgressBarModule, |
||||
|
MatTableModule, |
||||
|
ReactiveFormsModule |
||||
|
], |
||||
|
``` |
||||
|
|
||||
|
Add this constructor body after dependency injection parameters: |
||||
|
|
||||
|
```ts |
||||
|
) { |
||||
|
addIcons({ createOutline, trashOutline }); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 4: Replace budget row action markup** |
||||
|
|
||||
|
In `budget-page.html`, replace the two budget action button bodies with: |
||||
|
|
||||
|
```html |
||||
|
<button |
||||
|
class="budget-action-button" |
||||
|
mat-icon-button |
||||
|
type="button" |
||||
|
[attr.aria-label]="'Edit budget'" |
||||
|
(click)="onUpdateBudget(budget)" |
||||
|
> |
||||
|
<ion-icon aria-hidden="true" name="create-outline" /> |
||||
|
</button> |
||||
|
<button |
||||
|
class="budget-action-button" |
||||
|
mat-icon-button |
||||
|
type="button" |
||||
|
[attr.aria-label]="'Delete budget'" |
||||
|
(click)="onDeleteBudget(budget.id)" |
||||
|
> |
||||
|
<ion-icon aria-hidden="true" name="trash-outline" /> |
||||
|
</button> |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 5: Add stable budget table action CSS** |
||||
|
|
||||
|
Append to `budget-page.scss`: |
||||
|
|
||||
|
```scss |
||||
|
.budget-table-wrapper { |
||||
|
overflow-x: auto; |
||||
|
} |
||||
|
|
||||
|
.budget-table { |
||||
|
min-width: 56rem; |
||||
|
} |
||||
|
|
||||
|
.budget-table .mat-column-actions { |
||||
|
white-space: nowrap; |
||||
|
width: 6rem; |
||||
|
} |
||||
|
|
||||
|
.budget-action-button { |
||||
|
height: 2.25rem; |
||||
|
line-height: 2.25rem; |
||||
|
width: 2.25rem; |
||||
|
} |
||||
|
|
||||
|
.budget-action-button ion-icon { |
||||
|
font-size: 1.125rem; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Wrap the table in `budget-page.html`: |
||||
|
|
||||
|
```html |
||||
|
<div class="budget-table-wrapper"> |
||||
|
<table class="budget-table gf-table w-100" mat-table [dataSource]="dataSource"> |
||||
|
... |
||||
|
</table> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 6: Run the budget page spec** |
||||
|
|
||||
|
Run: |
||||
|
|
||||
|
```bash |
||||
|
./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts --runInBand --watchman=false |
||||
|
``` |
||||
|
|
||||
|
Expected: PASS. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Task 4: Visual Verification In Browser |
||||
|
|
||||
|
**Files:** |
||||
|
|
||||
|
- No source edits expected unless verification finds a regression. |
||||
|
|
||||
|
- [ ] **Step 1: Start the dev server** |
||||
|
|
||||
|
Run: |
||||
|
|
||||
|
```bash |
||||
|
pnpm start:dev |
||||
|
``` |
||||
|
|
||||
|
If the project uses the existing Nx command instead, run: |
||||
|
|
||||
|
```bash |
||||
|
./node_modules/.bin/nx run client:serve:development-en --hmr -o |
||||
|
``` |
||||
|
|
||||
|
Expected: Budget page is available on the local dev server. |
||||
|
|
||||
|
- [ ] **Step 2: Open Budget page and Manage categories** |
||||
|
|
||||
|
Use the browser to navigate to the Budget page, then click `Manage categories`. |
||||
|
|
||||
|
Expected: |
||||
|
|
||||
|
- Dialog width is about `42rem` on desktop. |
||||
|
- Form fields and Create button are aligned on one row at desktop width. |
||||
|
- Table actions are two compact icon buttons, not visible `edit delete` text. |
||||
|
- The close button remains visible and aligned to the right. |
||||
|
|
||||
|
- [ ] **Step 3: Verify narrow width** |
||||
|
|
||||
|
Resize the viewport below `768px`. |
||||
|
|
||||
|
Expected: |
||||
|
|
||||
|
- Category form stacks vertically. |
||||
|
- Table can scroll horizontally if needed. |
||||
|
- No controls overlap or clip text. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Task 5: Final Verification And Commit |
||||
|
|
||||
|
**Files:** |
||||
|
|
||||
|
- All modified Budget UI files. |
||||
|
|
||||
|
- [ ] **Step 1: Run focused tests** |
||||
|
|
||||
|
Run: |
||||
|
|
||||
|
```bash |
||||
|
./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.spec.ts --runInBand --watchman=false |
||||
|
./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts --runInBand --watchman=false |
||||
|
``` |
||||
|
|
||||
|
Expected: both PASS. |
||||
|
|
||||
|
- [ ] **Step 2: Run Angular compiler** |
||||
|
|
||||
|
Run: |
||||
|
|
||||
|
```bash |
||||
|
./node_modules/.bin/ngc -p apps/client/tsconfig.app.json |
||||
|
``` |
||||
|
|
||||
|
Expected: exits with code 0. |
||||
|
|
||||
|
- [ ] **Step 3: Run client build if the local environment allows it** |
||||
|
|
||||
|
Run: |
||||
|
|
||||
|
```bash |
||||
|
./node_modules/.bin/nx run client:build:development-en |
||||
|
``` |
||||
|
|
||||
|
Expected: PASS. If it still exits without diagnostics in this sandbox, record that `ngc` and focused specs passed, then rerun in the normal user terminal before merging. |
||||
|
|
||||
|
- [ ] **Step 4: Commit** |
||||
|
|
||||
|
Run: |
||||
|
|
||||
|
```bash |
||||
|
git add apps/client/src/app/pages/portfolio/budget |
||||
|
git commit -m "fix: repair budget category dialog layout" |
||||
|
``` |
||||
|
|
||||
|
Expected: commit succeeds. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Self-Review |
||||
|
|
||||
|
- Spec coverage: The plan directly addresses the visible broken category dialog, icon text overflow, table action clipping, and responsive layout. |
||||
|
- Placeholder scan: No `TODO`, `TBD`, or unspecified implementation steps remain. |
||||
|
- Type consistency: Uses `IonIcon`, `addIcons`, `createOutline`, and `trashOutline` consistently in both Budget page and category dialog. |
||||
File diff suppressed because it is too large
Loading…
Reference in new issue