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.
153 lines
4.4 KiB
TypeScript
153 lines
4.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { get } from '../../api/client';
|
|
import type { Project } from '../../api/types';
|
|
import type { ItemFilters } from '../../hooks/useItems';
|
|
|
|
interface ItemsToolbarProps {
|
|
filters: ItemFilters;
|
|
onFilterChange: (partial: Partial<ItemFilters>) => void;
|
|
layout: 'horizontal' | 'vertical';
|
|
onLayoutChange: (layout: 'horizontal' | 'vertical') => void;
|
|
onExport: () => void;
|
|
onImport: () => void;
|
|
onCreate: () => void;
|
|
isEditor: boolean;
|
|
}
|
|
|
|
export function ItemsToolbar({
|
|
filters, onFilterChange, layout, onLayoutChange,
|
|
onExport, onImport, onCreate, isEditor,
|
|
}: ItemsToolbarProps) {
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
|
|
useEffect(() => {
|
|
get<Project[]>('/api/projects').then(setProjects).catch(() => {});
|
|
}, []);
|
|
|
|
const scopeBtn = (scope: ItemFilters['searchScope'], label: string) => (
|
|
<button
|
|
onClick={() => onFilterChange({ searchScope: scope })}
|
|
style={{
|
|
padding: '0.3rem 0.6rem',
|
|
fontSize: '0.8rem',
|
|
border: 'none',
|
|
borderRadius: '0.3rem',
|
|
cursor: 'pointer',
|
|
backgroundColor: filters.searchScope === scope ? 'var(--ctp-mauve)' : 'var(--ctp-surface1)',
|
|
color: filters.searchScope === scope ? 'var(--ctp-crust)' : 'var(--ctp-subtext1)',
|
|
}}
|
|
>
|
|
{label}
|
|
</button>
|
|
);
|
|
|
|
return (
|
|
<div style={{
|
|
display: 'flex',
|
|
flexWrap: 'wrap',
|
|
gap: '0.75rem',
|
|
alignItems: 'center',
|
|
padding: '0.75rem 0',
|
|
borderBottom: '1px solid var(--ctp-surface0)',
|
|
marginBottom: '0.5rem',
|
|
}}>
|
|
{/* Search */}
|
|
<input
|
|
type="text"
|
|
placeholder="Search items... (Ctrl+F)"
|
|
value={filters.search}
|
|
onChange={(e) => onFilterChange({ search: e.target.value })}
|
|
style={{
|
|
flex: 1,
|
|
minWidth: 200,
|
|
padding: '0.4rem 0.75rem',
|
|
backgroundColor: 'var(--ctp-surface0)',
|
|
border: '1px solid var(--ctp-surface1)',
|
|
borderRadius: '0.4rem',
|
|
color: 'var(--ctp-text)',
|
|
fontSize: '0.85rem',
|
|
}}
|
|
/>
|
|
|
|
{/* Search scope */}
|
|
<div style={{ display: 'flex', gap: '0.25rem' }}>
|
|
{scopeBtn('all', 'All')}
|
|
{scopeBtn('part_number', 'PN')}
|
|
{scopeBtn('description', 'Desc')}
|
|
</div>
|
|
|
|
{/* Type filter */}
|
|
<select
|
|
value={filters.type}
|
|
onChange={(e) => onFilterChange({ type: e.target.value })}
|
|
style={selectStyle}
|
|
>
|
|
<option value="">All Types</option>
|
|
<option value="part">Part</option>
|
|
<option value="assembly">Assembly</option>
|
|
<option value="document">Document</option>
|
|
<option value="tooling">Tooling</option>
|
|
</select>
|
|
|
|
{/* Project filter */}
|
|
<select
|
|
value={filters.project}
|
|
onChange={(e) => onFilterChange({ project: e.target.value })}
|
|
style={selectStyle}
|
|
>
|
|
<option value="">All Projects</option>
|
|
{projects.map((p) => (
|
|
<option key={p.code} value={p.code}>{p.code}{p.name ? ` — ${p.name}` : ''}</option>
|
|
))}
|
|
</select>
|
|
|
|
{/* Layout toggle */}
|
|
<button
|
|
onClick={() => onLayoutChange(layout === 'horizontal' ? 'vertical' : 'horizontal')}
|
|
title={`Switch to ${layout === 'horizontal' ? 'vertical' : 'horizontal'} layout`}
|
|
style={toolBtnStyle}
|
|
>
|
|
{layout === 'horizontal' ? '⬌' : '⬍'}
|
|
</button>
|
|
|
|
{/* Export */}
|
|
<button onClick={onExport} style={toolBtnStyle} title="Export CSV">Export</button>
|
|
|
|
{/* Import (editor only) */}
|
|
{isEditor && (
|
|
<button onClick={onImport} style={toolBtnStyle} title="Import CSV">Import</button>
|
|
)}
|
|
|
|
{/* Create (editor only) */}
|
|
{isEditor && (
|
|
<button onClick={onCreate} style={{
|
|
...toolBtnStyle,
|
|
backgroundColor: 'var(--ctp-mauve)',
|
|
color: 'var(--ctp-crust)',
|
|
}}>
|
|
+ New
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const selectStyle: React.CSSProperties = {
|
|
padding: '0.4rem 0.6rem',
|
|
backgroundColor: 'var(--ctp-surface0)',
|
|
border: '1px solid var(--ctp-surface1)',
|
|
borderRadius: '0.4rem',
|
|
color: 'var(--ctp-text)',
|
|
fontSize: '0.85rem',
|
|
};
|
|
|
|
const toolBtnStyle: React.CSSProperties = {
|
|
padding: '0.4rem 0.75rem',
|
|
backgroundColor: 'var(--ctp-surface1)',
|
|
border: 'none',
|
|
borderRadius: '0.4rem',
|
|
color: 'var(--ctp-text)',
|
|
fontSize: '0.85rem',
|
|
cursor: 'pointer',
|
|
};
|