feat(web): migrate Items page to React with UI improvements

Phase 2 of frontend migration (epic #6, issue #8).

Rebuild the Items page (4,243 lines of vanilla JS) as 16 React
components with full feature parity plus UI improvements.

UI improvements:
- Footer stats bar (28px fixed bottom) replacing top stat cards
- Compact row density (28-32px) with alternating background colors
- Right-click column configuration via reusable ContextMenu component
- Resizable horizontal/vertical split panel layout (persisted)
- In-pane CRUD forms replacing modal dialogs (Infor-style)

Components (web/src/components/items/):
- ItemTable: sortable columns, alternating rows, column config
- ItemsToolbar: search with scope (All/PN/Desc), filters, actions
- SplitPanel: drag-resizable horizontal/vertical container
- FooterStats: fixed bottom bar with reactive item counts
- ItemDetail: 5-tab detail pane (Main, Properties, Revisions, BOM,
  Where Used) with header actions
- MainTab: metadata, inline project tag editor, file download
- PropertiesTab: form/JSON dual-mode editor, save as new revision
- RevisionsTab: comparison diff, status management, rollback
- BOMTab: inline CRUD, cost calculations, CSV export
- WhereUsedTab: parent assemblies table
- CreateItemPane: in-pane form with schema category properties
- EditItemPane: in-pane edit form for basic fields
- DeleteItemPane: in-pane confirmation with warning
- ImportItemsPane: CSV upload with dry-run validation flow

Shared components:
- ContextMenu: positioned right-click menu with checkbox support

Hooks:
- useItems: items fetching with search, filters, pagination, debounce
- useLocalStorage: typed localStorage state hook

Extended api/types.ts with request/response types for search, BOM,
revisions, CSV import, schema properties, and revision comparison.
This commit is contained in:
Forbes
2026-02-06 17:21:18 -06:00
parent 118c32dc14
commit a4f32b2b49
19 changed files with 2846 additions and 58 deletions

View File

@@ -3,7 +3,7 @@ export interface User {
username: string;
display_name: string;
email: string;
role: 'admin' | 'editor' | 'viewer';
role: "admin" | "editor" | "viewer";
auth_source: string;
}
@@ -127,3 +127,104 @@ export interface ErrorResponse {
error: string;
message?: string;
}
// Search
export interface FuzzyResult extends Item {
score: number;
}
// Where Used
export interface WhereUsedEntry {
id: string;
parent_part_number: string;
parent_description: string;
rel_type: string;
quantity: number | null;
unit?: string;
reference_designators?: string[];
}
// CSV Import
export interface CSVImportResult {
total_rows: number;
success_count: number;
error_count: number;
errors?: CSVImportError[];
created_items?: string[];
}
export interface CSVImportError {
row: number;
field?: string;
message: string;
}
// Request types
export interface CreateItemRequest {
schema?: string;
category: string;
description: string;
projects?: string[];
properties?: Record<string, unknown>;
sourcing_type?: string;
sourcing_link?: string;
long_description?: string;
standard_cost?: number;
}
export interface UpdateItemRequest {
part_number?: string;
item_type?: string;
description?: string;
properties?: Record<string, unknown>;
comment?: string;
sourcing_type?: string;
sourcing_link?: string;
long_description?: string;
standard_cost?: number;
}
export interface CreateRevisionRequest {
properties: Record<string, unknown>;
comment: string;
}
export interface AddBOMEntryRequest {
child_part_number: string;
rel_type?: string;
quantity?: number;
unit?: string;
reference_designators?: string[];
child_revision?: number;
metadata?: Record<string, unknown>;
}
export interface UpdateBOMEntryRequest {
rel_type?: string;
quantity?: number;
unit?: string;
reference_designators?: string[];
child_revision?: number;
metadata?: Record<string, unknown>;
}
// Schema properties
export interface PropertyDef {
type: string;
required?: boolean;
description?: string;
default?: unknown;
}
export type PropertySchema = Record<string, PropertyDef>;
// Revision comparison
export interface RevisionComparison {
from: number;
to: number;
added: Record<string, unknown>;
removed: Record<string, unknown>;
changed: Record<string, { from: unknown; to: unknown }>;
status_changed?: { from: string; to: string };
file_changed?: boolean;
}