feat: add undo button in DimensionsView for pending changes

This commit is contained in:
Christian Vidal Wolf
2026-04-09 09:13:18 +02:00
parent 1b0b35d3bf
commit 2ea5bf80aa
3 changed files with 28 additions and 83 deletions
+11 -77
View File
@@ -15,7 +15,7 @@ npm run clean # Remove dist folder
npm run lint # TypeScript type check only (tsc --noEmit) npm run lint # TypeScript type check only (tsc --noEmit)
``` ```
Note: This project does not have a separate test framework configured. To add tests, consider installing Vitest or Jest. Note: No test framework configured. To add tests, install Vitest or Jest.
--- ---
@@ -27,10 +27,8 @@ Note: This project does not have a separate test framework configured. To add te
- Keep components focused and modular - Keep components focused and modular
- Use meaningful variable and function names - Use meaningful variable and function names
### Imports ### Imports (order top to bottom)
1. React (`react`)
**Order (top to bottom):**
1. React imports (`react`)
2. External libraries (`lucide-react`, `xlsx`, etc.) 2. External libraries (`lucide-react`, `xlsx`, etc.)
3. Internal components (`./components/...`) 3. Internal components (`./components/...`)
4. Internal lib/utils (`./lib/...`) 4. Internal lib/utils (`./lib/...`)
@@ -44,19 +42,16 @@ import { cn } from '../lib/utils';
``` ```
### TypeScript Conventions ### TypeScript Conventions
- Use explicit types for props and function parameters - Use explicit types for props and function parameters
- Use `any` sparingly; prefer union types or interfaces - Use `any` sparingly; prefer union types or interfaces
- Define column indices in a centralized `COLUMNS` object (see `src/types.ts`) - Define column indices in a centralized `COLUMNS` object (see `src/types.ts`)
```typescript ```typescript
// Good
interface ProductDescriptionsProps { interface ProductDescriptionsProps {
data: ExcelRow[]; data: ExcelRow[];
onEdit: (index: number) => void; onEdit: (index: number) => void;
} }
// Good - centralized constants
export const COLUMNS = { export const COLUMNS = {
ARTICLE_NO: 0, ARTICLE_NO: 0,
ARTICLE_NAME: 2, ARTICLE_NAME: 2,
@@ -76,119 +71,58 @@ export const COLUMNS = {
| Types | PascalCase | `TabType`, `SortDirection` | | Types | PascalCase | `TabType`, `SortDirection` |
### React Patterns ### React Patterns
- Destructure props in function signature - Destructure props in function signature
- Use `useMemo` for expensive computations - Use `useMemo` for expensive computations
- Use `useCallback` for event handlers passed to child components - Use `useCallback` for event handlers passed to child components
- Keep `useState` calls at the top of component - Keep `useState` calls at the top of component
```typescript
export function ProductDescriptions({ data, onEdit }: ProductDescriptionsProps) {
const [activeTab, setActiveTab] = useState<TabType>('all');
const [search, setSearch] = useState('');
const filteredData = useMemo(() => {
// expensive computation
}, [data, activeTab, search]);
}
```
### Error Handling ### Error Handling
- Use TypeScript's type system for runtime safety - Use TypeScript's type system for runtime safety
- Use optional chaining (`?.`) and nullish coalescing (`??`) - Use optional chaining (`?.`) and nullish coalescing (`??`)
- Validate file uploads with proper type checks - Validate file uploads with proper type checks
```typescript
const file = e.target.files?.[0];
if (!file) return;
// Validate Excel data
if (data.length > 0) {
const rawHeaders = data[0];
const rawRows = data.slice(1);
}
```
### UI/Styling ### UI/Styling
- Use Tailwind CSS for all styling - Use Tailwind CSS for all styling
- Use `cn()` utility from `lib/utils` for conditional classes - Use `cn()` utility from `lib/utils` for conditional classes
- Follow existing color scheme (slate, blue, green, red for status) - Follow existing color scheme (slate, blue, green, red for status)
- Use `lucide-react` for icons - Use `lucide-react` for icons
- Keep responsive design in mind
```typescript ```typescript
import { cn } from '../lib/utils'; <button className={cn("px-4 py-2 rounded-md", isActive ? "bg-blue-600" : "bg-slate-800")}>
<button
className={cn(
"px-4 py-2 rounded-md text-sm font-medium",
isActive
? "bg-blue-600 text-white"
: "bg-slate-800 text-slate-400"
)}
>
``` ```
### File Organization ### File Organization
``` ```
src/ src/
├── components/ # React components ├── components/ # React components
│ ├── Sidebar.tsx ├── lib/ # Utilities (utils.ts, auth.ts, supabase.ts)
│ ├── MatrixView.tsx ├── services/ # External API integrations (gemini.ts, anthropic.ts)
│ └── ...
├── lib/ # Utilities and helpers
│ ├── utils.ts # cn(), helpers
│ ├── auth.ts # Authentication
│ └── supabase.ts # Database operations
├── services/ # External API integrations
│ ├── gemini.ts
│ └── anthropic.ts
├── types.ts # TypeScript types and constants ├── types.ts # TypeScript types and constants
├── App.tsx # Main application ├── App.tsx # Main application
└── main.tsx # Entry point └── main.tsx # Entry point
``` ```
### Data Processing ### Data Processing
- Handle both string and number types when processing Excel data
- When processing Excel data, handle both string and number types
- Use centralized column index constants - Use centralized column index constants
- Format numbers consistently (2 decimal places for prices/weights) - Format numbers consistently (2 decimal places for prices/weights)
- Handle Excel date serial numbers properly (convert to readable dates) - Handle Excel date serial numbers properly
```typescript ```typescript
// Handle date columns from Excel // Handle date columns from Excel
if (header.includes('date') || header.includes('launch')) { if (typeof val === 'number' && val >= 25569 && val <= 60000) {
if (typeof val === 'number' && val >= 25569 && val <= 60000) {
const excelEpoch = new Date(1899, 11, 30); const excelEpoch = new Date(1899, 11, 30);
const date = new Date(excelEpoch.getTime() + val * 86400000); return new Date(excelEpoch.getTime() + val * 86400000).toLocaleDateString('en-GB');
return date.toLocaleDateString('en-GB');
}
} }
``` ```
### Git Workflow ### Git Workflow
- Make small, focused commits - Make small, focused commits
- Write clear commit messages describing what changed - Write clear commit messages describing what changed
- Push to main to trigger Vercel deployment automatically - Push to main to trigger Vercel deployment automatically
### Running Single Components
When testing or developing specific features:
```bash
npm run dev # Start dev server - access at http://localhost:3000
```
### Environment Variables ### Environment Variables
- Use `.env` file for local development - Use `.env` file for local development
- Never commit secrets - use Vercel dashboard for production env vars - Never commit secrets - use Vercel dashboard for production env vars
Required environment variables (production): Required: `VITE_SUPABASE_URL`, `VITE_SUPABASE_ANON_KEY`, `VITE_GEMINI_API_KEY`, `VITE_ANTHROPIC_API_KEY`
- `VITE_SUPABASE_URL`
- `VITE_SUPABASE_ANON_KEY`
- `VITE_GEMINI_API_KEY`
- `VITE_ANTHROPIC_API_KEY`
+1
View File
@@ -414,6 +414,7 @@ export default function App() {
onSaveRow={handleSaveRow} onSaveRow={handleSaveRow}
onCaptureState={captureState} onCaptureState={captureState}
rowStatuses={rowStatuses} rowStatuses={rowStatuses}
onRevertRow={handleRevertRow}
/> />
)} )}
+12 -2
View File
@@ -1,6 +1,6 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import { ExcelRow, COLUMNS } from '../types'; import { ExcelRow, COLUMNS } from '../types';
import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Edit2, Package, Boxes, Scale, Loader2, RefreshCw, Layers, Link2, Search, Filter, X as XIcon } from 'lucide-react'; import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Edit2, Package, Boxes, Scale, Loader2, RefreshCw, Layers, Link2, Search, Filter, X as XIcon, Undo2 } from 'lucide-react';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import { ConfirmModal } from './ConfirmModal'; import { ConfirmModal } from './ConfirmModal';
import { ColumnFilterPopover } from './ColumnFilterPopover'; import { ColumnFilterPopover } from './ColumnFilterPopover';
@@ -12,6 +12,7 @@ interface DimensionsViewProps {
onSaveRow: (index: number, updatedRow: ExcelRow) => void; onSaveRow: (index: number, updatedRow: ExcelRow) => void;
onCaptureState: (message: string) => void; onCaptureState: (message: string) => void;
rowStatuses: Record<string, string>; rowStatuses: Record<string, string>;
onRevertRow: (articleNo: string) => void;
} }
interface DimensionGroup { interface DimensionGroup {
@@ -33,7 +34,7 @@ interface NearDuplicateCluster {
maxDiffPct: number; maxDiffPct: number;
} }
export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureState, rowStatuses }: DimensionsViewProps) { export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureState, rowStatuses, onRevertRow }: DimensionsViewProps) {
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set()); const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
const [expandedNearDuplicates, setExpandedNearDuplicates] = useState<Set<number>>(new Set()); const [expandedNearDuplicates, setExpandedNearDuplicates] = useState<Set<number>>(new Set());
const [showOnlyInconsistent, setShowOnlyInconsistent] = useState(true); const [showOnlyInconsistent, setShowOnlyInconsistent] = useState(true);
@@ -672,6 +673,15 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
> >
{syncing?.key === group.key && syncing?.field === 'all' ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />} {syncing?.key === group.key && syncing?.field === 'all' ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
</button> </button>
{isPending && (
<button
onClick={() => onRevertRow(String(row[COLUMNS.ARTICLE_NO]))}
title="Undo pending changes"
className="p-1.5 hover:bg-red-600/20 text-slate-500 hover:text-red-400 rounded transition-all"
>
<Undo2 className="w-4 h-4" />
</button>
)}
<button <button
onClick={() => onEdit(index)} onClick={() => onEdit(index)}
title="Edit product" title="Edit product"