diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..0a7771b --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,289 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/hook-handler.cjs\" pre-bash'", + "timeout": 5000 + } + ] + }, + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/hook-handler.cjs\" pre-edit'", + "timeout": 5000 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/hook-handler.cjs\" post-edit'", + "timeout": 10000 + } + ] + }, + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/hook-handler.cjs\" post-bash'", + "timeout": 5000 + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/hook-handler.cjs\" route'", + "timeout": 10000 + } + ] + } + ], + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/hook-handler.cjs\" session-restore'", + "timeout": 15000 + }, + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/auto-memory-hook.mjs\" import'", + "timeout": 8000 + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/hook-handler.cjs\" session-end'", + "timeout": 10000 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/auto-memory-hook.mjs\" sync'", + "timeout": 10000 + } + ] + } + ], + "PreCompact": [ + { + "matcher": "manual", + "hooks": [ + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/hook-handler.cjs\" compact-manual'" + }, + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/hook-handler.cjs\" session-end'", + "timeout": 5000 + } + ] + }, + { + "matcher": "auto", + "hooks": [ + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/hook-handler.cjs\" compact-auto'" + }, + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/hook-handler.cjs\" session-end'", + "timeout": 6000 + } + ] + } + ], + "SubagentStart": [ + { + "hooks": [ + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/hook-handler.cjs\" status'", + "timeout": 3000 + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/hook-handler.cjs\" post-task'", + "timeout": 5000 + } + ] + } + ], + "Notification": [ + { + "hooks": [ + { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/hook-handler.cjs\" notify'", + "timeout": 3000 + } + ] + } + ] + }, + "statusLine": { + "type": "command", + "command": "sh -c 'exec node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/statusline.cjs\"'" + }, + "permissions": { + "allow": [ + "Bash(npx @claude-flow*)", + "Bash(npx claude-flow*)", + "Bash(node .claude/*)", + "mcp__claude-flow__:*" + ], + "deny": [ + "Read(./.env)", + "Read(./.env.*)" + ] + }, + "attribution": { + "commit": "Co-Authored-By: claude-flow ", + "pr": "πŸ€– Generated with [claude-flow](https://github.com/ruvnet/claude-flow)" + }, + "env": { + "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1", + "CLAUDE_FLOW_V3_ENABLED": "true", + "CLAUDE_FLOW_HOOKS_ENABLED": "true" + }, + "claudeFlow": { + "version": "3.0.0", + "enabled": true, + "platform": { + "os": "darwin", + "arch": "arm64", + "shell": "zsh" + }, + "modelPreferences": { + "default": "claude-opus-4-6", + "routing": "claude-haiku-4-5-20251001" + }, + "agentTeams": { + "enabled": true, + "teammateMode": "auto", + "taskListEnabled": true, + "mailboxEnabled": true, + "coordination": { + "autoAssignOnIdle": true, + "trainPatternsOnComplete": true, + "notifyLeadOnComplete": true, + "sharedMemoryNamespace": "agent-teams" + }, + "hooks": { + "teammateIdle": { + "enabled": true, + "autoAssign": true, + "checkTaskList": true + }, + "taskCompleted": { + "enabled": true, + "trainPatterns": true, + "notifyLead": true + } + } + }, + "swarm": { + "topology": "hierarchical-mesh", + "maxAgents": 15 + }, + "memory": { + "backend": "hybrid", + "enableHNSW": true, + "learningBridge": { + "enabled": true + }, + "memoryGraph": { + "enabled": true + }, + "agentScopes": { + "enabled": true + } + }, + "neural": { + "enabled": true + }, + "daemon": { + "autoStart": false, + "workers": [ + "map", + "audit", + "optimize" + ], + "schedules": { + "audit": { + "interval": "4h", + "priority": "critical" + }, + "optimize": { + "interval": "2h", + "priority": "high" + } + } + }, + "learning": { + "enabled": true, + "autoTrain": true, + "patterns": [ + "coordination", + "optimization", + "prediction" + ], + "retention": { + "shortTerm": "24h", + "longTerm": "30d" + } + }, + "adr": { + "autoGenerate": true, + "directory": "/docs/adr", + "template": "madr" + }, + "ddd": { + "trackDomains": true, + "validateBoundedContexts": true, + "directory": "/docs/ddd" + }, + "security": { + "autoScan": true, + "scanOnEdit": true, + "cveCheck": true, + "threatModel": true + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index bf12ddb..c727d26 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,15 @@ coverage/ !.env.example .vercel .env*.local + +# RuFlo / Claude-Flow +.claude-flow/ +.claude/agents/ +.claude/helpers/ +.claude/commands/ +.claude/skills/ +.agents/ +.swarm/ +agentdb.rvf* +ruvector.db +claude-flow.config.json diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..1f54617 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,22 @@ +{ + "mcpServers": { + "claude-flow": { + "command": "npx", + "args": [ + "-y", + "@claude-flow/cli@latest", + "mcp", + "start" + ], + "env": { + "npm_config_update_notifier": "false", + "CLAUDE_FLOW_MODE": "v3", + "CLAUDE_FLOW_HOOKS_ENABLED": "true", + "CLAUDE_FLOW_TOPOLOGY": "hierarchical-mesh", + "CLAUDE_FLOW_MAX_AGENTS": "15", + "CLAUDE_FLOW_MEMORY_BACKEND": "hybrid" + }, + "autoStart": false + } + } +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index dcb150d..3ee93cc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,16 @@ # AGENTS.md - Developer Guidelines for Craze-Data-check +## Ruflo Standard Operating Procedure (SOP) +**MANDATORY: Follow these steps for every user instruction.** + +1. **Retrieve (Semantic Memory)**: Before starting, run `ruflo memory search -q ""` to find relevant historical patterns or previous solutions. +2. **Verify (Security)**: Use `ruflo security defend -i ""` to ensure the instruction is safe and follows security protocols. +3. **Execute (Agent Swarm)**: For complex tasks (refactors, multi-file changes), consider spawning specialized agents with `ruflo agent spawn -t `. +4. **Version Control**: Use `agentic-jujutsu` for managing concurrent changes or if a lock-free approach is needed. +5. **Record (Learning)**: After completion, record the outcome with `ruflo memory store --key "" --value ""` to improve future performance. + +--- + ## Build, Lint, and Test Commands ### Development diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f0d9325 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,244 @@ +# Claude Code Configuration - RuFlo V3 + +## Behavioral Rules (Always Enforced) + +- Do what has been asked; nothing more, nothing less +- NEVER create files unless they're absolutely necessary for achieving your goal +- ALWAYS prefer editing an existing file to creating a new one +- NEVER proactively create documentation files (*.md) or README files unless explicitly requested +- NEVER save working files, text/mds, or tests to the root folder +- Never continuously check status after spawning a swarm β€” wait for results +- ALWAYS read a file before editing it +- NEVER commit secrets, credentials, or .env files + +## File Organization + +- NEVER save to root folder β€” use the directories below +- Use `/src` for source code files +- Use `/tests` for test files +- Use `/docs` for documentation and markdown files +- Use `/config` for configuration files +- Use `/scripts` for utility scripts +- Use `/examples` for example code + +## Project Architecture + +- Follow Domain-Driven Design with bounded contexts +- Keep files under 500 lines +- Use typed interfaces for all public APIs +- Prefer TDD London School (mock-first) for new code +- Use event sourcing for state changes +- Ensure input validation at system boundaries + +### Project Config + +- **Topology**: hierarchical-mesh +- **Max Agents**: 15 +- **Memory**: hybrid +- **HNSW**: Enabled +- **Neural**: Enabled + +## Build & Test + +```bash +# Build +npm run build + +# Test +npm test + +# Lint +npm run lint +``` + +- ALWAYS run tests after making code changes +- ALWAYS verify build succeeds before committing + +## Security Rules + +- NEVER hardcode API keys, secrets, or credentials in source files +- NEVER commit .env files or any file containing secrets +- Always validate user input at system boundaries +- Always sanitize file paths to prevent directory traversal +- Run `npx @claude-flow/cli@latest security scan` after security-related changes + +## Concurrency: 1 MESSAGE = ALL RELATED OPERATIONS + +- All operations MUST be concurrent/parallel in a single message +- Use Claude Code's Agent tool for spawning agents, not just MCP +- ALWAYS spawn ALL agents in ONE message with full instructions via Agent tool +- ALWAYS batch ALL file reads/writes/edits in ONE message +- ALWAYS batch ALL Bash commands in ONE message + +## Swarm Orchestration + +- MUST initialize the swarm using CLI tools when starting complex tasks +- MUST spawn concurrent agents using Claude Code's Agent tool +- Never use CLI tools alone for execution β€” Agent tool agents do the actual work +- MUST call CLI tools AND Agent tool in ONE message for complex work + +### 3-Tier Model Routing (ADR-026) + +| Tier | Handler | Latency | Cost | Use Cases | +|------|---------|---------|------|-----------| +| **1** | Agent Booster (WASM) | <1ms | $0 | Simple transforms (varβ†’const, add types) β€” Skip LLM | +| **2** | Haiku | ~500ms | $0.0002 | Simple tasks, low complexity (<30%) | +| **3** | Sonnet/Opus | 2-5s | $0.003-0.015 | Complex reasoning, architecture, security (>30%) | + +- For Tier 1 simple transforms, use Edit tool directly β€” no LLM agent needed + +## Swarm Configuration & Anti-Drift + +- ALWAYS use hierarchical topology for coding swarms +- Keep maxAgents at 6-8 for tight coordination +- Use specialized strategy for clear role boundaries +- Use `raft` consensus for hive-mind (leader maintains authoritative state) +- Run frequent checkpoints via `post-task` hooks +- Keep shared memory namespace for all agents + +```bash +npx @claude-flow/cli@latest swarm init --topology hierarchical --max-agents 8 --strategy specialized +``` + +## Swarm Execution Rules + +- ALWAYS use `run_in_background: true` for all Agent tool calls +- ALWAYS put ALL Agent calls in ONE message for parallel execution +- After spawning, STOP β€” do NOT add more tool calls or check status +- Never poll agent status repeatedly β€” trust agents to return +- When agent results arrive, review ALL results before proceeding + +## V3 CLI Commands + +### Core Commands + +| Command | Subcommands | Description | +|---------|-------------|-------------| +| `init` | 4 | Project initialization | +| `agent` | 8 | Agent lifecycle management | +| `swarm` | 6 | Multi-agent swarm coordination | +| `memory` | 11 | AgentDB memory with HNSW search | +| `task` | 6 | Task creation and lifecycle | +| `session` | 7 | Session state management | +| `hooks` | 17 | Self-learning hooks + 12 workers | +| `hive-mind` | 6 | Byzantine fault-tolerant consensus | + +### Quick CLI Examples + +```bash +npx @claude-flow/cli@latest init --wizard +npx @claude-flow/cli@latest agent spawn -t coder --name my-coder +npx @claude-flow/cli@latest swarm init --v3-mode +npx @claude-flow/cli@latest memory search --query "authentication patterns" +npx @claude-flow/cli@latest doctor --fix +``` + +## Available Agents (16 Roles + Custom) + +### Core Development +`coder`, `reviewer`, `tester`, `planner`, `researcher` + +### Specialized +`security-architect`, `security-auditor`, `memory-specialist`, `performance-engineer` + +### Coordination +`hierarchical-coordinator`, `mesh-coordinator`, `adaptive-coordinator` + +### GitHub & Repository +`pr-manager`, `code-review-swarm`, `issue-tracker`, `release-manager` + +Any string can be used as a custom agent type β€” these are the typed roles with specialized behavior. + +## Memory & Vector Search + +### MCP Tools (use via ToolSearch to discover) + +| Tool | Description | +|------|-------------| +| `memory_store` | Store value with ONNX 384-dim vector embedding | +| `memory_search` | Semantic vector search by query | +| `memory_retrieve` | Get entry by key | +| `memory_list` | List entries in namespace | +| `memory_delete` | Delete entry | +| `memory_import_claude` | Import Claude Code memories into AgentDB (allProjects=true for all) | +| `memory_search_unified` | Search across ALL namespaces (Claude + AgentDB + patterns) | +| `memory_bridge_status` | Show bridge health, vectors, SONA, intelligence | + +### CLI Commands + +```bash +# Store with vector embedding +npx @claude-flow/cli@latest memory store --key "pattern-auth" --value "JWT with refresh" --namespace patterns + +# Semantic search +npx @claude-flow/cli@latest memory search --query "authentication patterns" + +# Import all Claude Code memories into AgentDB +node .claude/helpers/auto-memory-hook.mjs import-all +``` + +### Claude Code ↔ AgentDB Bridge + +Claude Code auto-memory files (`~/.claude/projects/*/memory/*.md`) are automatically imported into AgentDB with ONNX vector embeddings on session start. Use `memory_search_unified` to search across both stores. + +## Key MCP Tools (314 available β€” use ToolSearch to discover) + +### Most Used Tools + +| Category | Tools | What They Do | +|----------|-------|-------------| +| **Memory** | `memory_store`, `memory_search`, `memory_search_unified` | Store/search with ONNX vector embeddings | +| **Claude Bridge** | `memory_import_claude`, `memory_bridge_status` | Import Claude memories into AgentDB | +| **Swarm** | `swarm_init`, `swarm_status`, `swarm_health` | Multi-agent coordination | +| **Agents** | `agent_spawn`, `agent_list`, `agent_status` | Agent lifecycle | +| **Hive-Mind** | `hive-mind_init`, `hive-mind_spawn`, `hive-mind_consensus` | Byzantine/Raft consensus | +| **Hooks** | `hooks_route`, `hooks_session-start`, `hooks_post-task` | Task routing + learning | +| **Workers** | `hooks_worker-list`, `hooks_worker-dispatch` | 12 background workers | +| **Security** | `aidefence_scan`, `aidefence_is_safe` | Prompt injection detection | +| **Intelligence** | `hooks_intelligence`, `neural_status` | Pattern learning + SONA | + +### Swarm Capabilities + +- **Topologies**: hierarchical (anti-drift), mesh, ring, star, adaptive +- **Consensus**: Raft (leader-based), Byzantine (PBFT), Gossip (eventual) +- **Hive-Mind**: Queen-led coordination with spawn, broadcast, consensus voting, shared memory +- **12 Background Workers**: audit, optimize, testgaps, map, deepdive, document, refactor, benchmark, ultralearn, consolidate, predict, preload + +### Memory Capabilities + +- **ONNX Embeddings**: all-MiniLM-L6-v2, 384 dimensions β€” real neural vectors +- **DiskANN**: SSD-friendly vector search (8,000x faster insert than HNSW, perfect recall at 1K) +- **sql.js**: Cross-platform SQLite (WASM, no native compilation) +- **Claude Code Bridge**: Auto-imports MEMORY.md files into AgentDB on session start +- **Unified Search**: `memory_search_unified` searches Claude memories + AgentDB + patterns +- **SONA Learning**: Trajectory recording β†’ pattern extraction β†’ file persistence + +### How to Discover Tools + +Use ToolSearch to find specific tools: +``` +ToolSearch("memory search") β†’ memory_store, memory_search, memory_search_unified +ToolSearch("swarm") β†’ swarm_init, swarm_status, swarm_health, swarm_shutdown +ToolSearch("hive consensus") β†’ hive-mind_consensus, hive-mind_status +ToolSearch("+aidefence") β†’ aidefence_scan, aidefence_is_safe, aidefence_has_pii +``` + +## Quick Setup + +```bash +claude mcp add claude-flow -- npx -y @claude-flow/cli@latest +npx @claude-flow/cli@latest daemon start +npx @claude-flow/cli@latest doctor --fix +``` + +## Claude Code vs MCP Tools + +- **Claude Code Agent tool** handles execution: agents, file ops, code generation, git +- **MCP tools** (via ToolSearch) handle coordination: swarm, memory, hooks, routing, hive-mind +- **CLI commands** (via Bash) are the same tools with terminal output +- Use `ToolSearch("keyword")` to discover available MCP tools + +## Support + +- Documentation: https://github.com/ruvnet/ruflo +- Issues: https://github.com/ruvnet/ruflo/issues diff --git a/src/components/DimensionsView.tsx b/src/components/DimensionsView.tsx index 2dc4767..7dec711 100644 --- a/src/components/DimensionsView.tsx +++ b/src/components/DimensionsView.tsx @@ -148,10 +148,15 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat if (search || lineFilter.length > 0 || classFilter.length > 0) { const s = search.toLowerCase(); result = result.filter(g => { - const matchesSearch = !search || g.rows.some(({ row }) => - String(row[COLUMNS.ARTICLE_NO] || '').toLowerCase().includes(s) || - String(row[COLUMNS.ARTICLE_NAME] || '').toLowerCase().includes(s) - ); + const matchesSearch = !search || (() => { + const terms = search.toLowerCase().split(/\s+/).filter(Boolean); + if (terms.length === 0) return true; + return g.rows.some(({ row }) => { + const articleNo = String(row[COLUMNS.ARTICLE_NO] || '').toLowerCase(); + const articleName = String(row[COLUMNS.ARTICLE_NAME] || '').toLowerCase(); + return terms.every(term => articleNo.includes(term) || articleName.includes(term)); + }); + })(); const matchesLine = lineFilter.length === 0 || g.rows.some(({ row }) => lineFilter.includes(String(row[COLUMNS.LINE] || ''))); const matchesClass = classFilter.length === 0 || g.rows.some(({ row }) => classFilter.includes(String(row[COLUMNS.CLASSIFICATION] || ''))); diff --git a/src/components/HistoryView.tsx b/src/components/HistoryView.tsx index f9b69ac..e32994e 100644 --- a/src/components/HistoryView.tsx +++ b/src/components/HistoryView.tsx @@ -78,11 +78,13 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp }).format(date); }; - const filteredHistory = history.filter(entry => - entry.article_name?.toLowerCase().includes(search.toLowerCase()) || - entry.product_id.toLowerCase().includes(search.toLowerCase()) || - entry.changed_by?.toLowerCase().includes(search.toLowerCase()) - ); + const filteredHistory = history.filter(entry => { + const terms = search.toLowerCase().split(/\s+/).filter(Boolean); + if (terms.length === 0) return true; + + const searchableText = `${entry.article_name} ${entry.product_id} ${entry.changed_by}`.toLowerCase(); + return terms.every(term => searchableText.includes(term)); + }); if (loading) { return ( diff --git a/src/components/MissingDataView.tsx b/src/components/MissingDataView.tsx index 1997e3d..c4ce600 100644 --- a/src/components/MissingDataView.tsx +++ b/src/components/MissingDataView.tsx @@ -203,12 +203,16 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi } } + // Global search (SKU + Name) - Multi-word AND support if (search) { - const s = search.toLowerCase(); - result = result.filter(r => - String(r.row[COLUMNS.ARTICLE_NO] || '').toLowerCase().includes(s) || - String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase().includes(s) - ); + const terms = search.toLowerCase().split(/\s+/).filter(Boolean); + if (terms.length > 0) { + result = result.filter(r => { + const articleNo = String(r.row[COLUMNS.ARTICLE_NO] || '').toLowerCase(); + const articleName = String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase(); + return terms.every(term => articleNo.includes(term) || articleName.includes(term)); + }); + } } (Object.entries(columnFilters) as [string, string[]][]).forEach(([colIdx, filterValues]) => { diff --git a/src/components/PendingValidationView.tsx b/src/components/PendingValidationView.tsx index bd09a03..b74f5ec 100644 --- a/src/components/PendingValidationView.tsx +++ b/src/components/PendingValidationView.tsx @@ -19,12 +19,15 @@ export function PendingValidationView({ data, pendingRows, rowStatuses, onRevert const pendingEntries = Object.entries(pendingRows); - const filteredEntries = search - ? pendingEntries.filter(([articleNo, { articleName }]) => - articleNo.toLowerCase().includes(search.toLowerCase()) || - articleName.toLowerCase().includes(search.toLowerCase()) - ) - : pendingEntries; + const filteredEntries = (() => { + const terms = search.toLowerCase().split(/\s+/).filter(Boolean); + if (terms.length === 0) return pendingEntries; + + return pendingEntries.filter(([articleNo, { articleName }]) => { + const searchableText = `${articleNo} ${articleName}`.toLowerCase(); + return terms.every(term => searchableText.includes(term)); + }); + })(); const getFieldDiff = (original: ExcelRow, updated: ExcelRow, colIndex: number) => { const orig = original[colIndex]; diff --git a/src/components/PricingView.tsx b/src/components/PricingView.tsx index 426dc41..d4c6572 100644 --- a/src/components/PricingView.tsx +++ b/src/components/PricingView.tsx @@ -114,12 +114,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, const searchSuggestions = useMemo(() => { if (!search && selectedSearchItems.size === 0) return data.slice(0, 50); if (!search) return []; - const s = search.toLowerCase(); - return data.filter(r => - String(r[COLUMNS.ARTICLE_NO] || '').toLowerCase().includes(s) || - String(r[COLUMNS.ARTICLE_NAME] || '').toLowerCase().includes(s) - ).slice(0, 50); - }, [data, search, selectedSearchItems]); + + const terms = search.toLowerCase().split(/\s+/).filter(Boolean); + if (terms.length === 0) return data.slice(0, 50); + + return data.filter(r => { + const articleNo = String(r[COLUMNS.ARTICLE_NO] || '').toLowerCase(); + const articleName = String(r[COLUMNS.ARTICLE_NAME] || '').toLowerCase(); + return terms.every(term => articleNo.includes(term) || articleName.includes(term)); + }).slice(0, 50); + }, [data, search, selectedSearchItems, COLUMNS]); const handleSearchItemClick = (index: number) => { const newSelected = new Set(selectedSearchItems); @@ -310,13 +314,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, case 'units_errors': result = analyzedRows.filter(r => r.unitErrors.length > 0); break; } - // Global search (SKU + Name) + // Global search (SKU + Name) - Multi-word AND support if (search) { - const s = search.toLowerCase(); - result = result.filter(r => - String(r.row[COLUMNS.ARTICLE_NO] || '').toLowerCase().includes(s) || - String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase().includes(s) - ); + const terms = search.toLowerCase().split(/\s+/).filter(Boolean); + if (terms.length > 0) { + result = result.filter(r => { + const articleNo = String(r.row[COLUMNS.ARTICLE_NO] || '').toLowerCase(); + const articleName = String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase(); + return terms.every(term => articleNo.includes(term) || articleName.includes(term)); + }); + } } // Selected items from search dropdown diff --git a/src/components/ProductDescriptions.tsx b/src/components/ProductDescriptions.tsx index e5059ce..17c684e 100644 --- a/src/components/ProductDescriptions.tsx +++ b/src/components/ProductDescriptions.tsx @@ -65,11 +65,14 @@ export function ProductDescriptions({ data, headers, asinColumnIndex, onEdit, ro // Search filter if (search) { - const s = search.toLowerCase(); - result = result.filter(r => - String(r.row[COLUMNS.ARTICLE_NO] || '').toLowerCase().includes(s) || - String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase().includes(s) - ); + const terms = search.toLowerCase().split(/\s+/).filter(Boolean); + if (terms.length > 0) { + result = result.filter(r => { + const articleNo = String(r.row[COLUMNS.ARTICLE_NO] || '').toLowerCase(); + const articleName = String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase(); + return terms.every(term => articleNo.includes(term) || articleName.includes(term)); + }); + } } // Dropdown filters