diff --git a/.claude/agent-memory/react-expert-dev/MEMORY.md b/.claude/agent-memory/react-expert-dev/MEMORY.md new file mode 100644 index 0000000..637280b --- /dev/null +++ b/.claude/agent-memory/react-expert-dev/MEMORY.md @@ -0,0 +1,52 @@ +# ScalesApp React Expert Memory + +## Key File Paths + +- Report Editor root: `frontend/src/components/ReportEditor/` +- Element models: `models/Element.jsx` — OOP class hierarchy, `fromJSON` has `elementClasses` map +- Element CSS: `elements.css` — one block per element type +- Inspector CSS: `ObjectInspector.css` +- Data resolver utils: `utils/dataResolver.js` — `resolveDBTextValue`, `getAvailableObjects`, `getAvailableFields` +- Expression evaluator: `utils/expressionEvaluator.js` — recursive-descent, no eval + +## Element Type Pattern + +Adding a new element type requires touching 8 files: +1. `models/Element.jsx` — new class + register in `elementClasses` map in `fromJSON` +2. `NewElement.jsx` — component file, modeled on `DBTextField.jsx` +3. `ExpressionEditorPanel.jsx` (or equivalent inspector panel) +4. `EditorCanvas.jsx` — import + `renderElement` case + click handler in `handleCanvasClick` +5. `Toolbar.jsx` — tool button +6. `ObjectInspector.jsx` — import panel + render for `element.type === 'xxx'` +7. `elements.css` — CSS block for new class +8. `ResizeHandles.jsx` — add type to `getHandles` switch, `getHandlePosition` branch, and resize logic branch + +## ObjectInspector Band-Detection Pattern + +When a panel needs to detect if an element is inside a band (for field resolution): +```js +const parentBand = allElements.find(el => el.type === 'band' && el.children && el.children.includes(element.id)); +// Nested band handling: +if (parentBand.parentBandId) { + const grandparentBand = allElements.find(el => el.id === parentBand.parentBandId); + // grandparentData[0][parentBand.dataSource] → nestedData array +} +``` + +## DBTextField / ExpressionField Props Pattern + +Both receive: `element, isSelected, isSingleSelection, onSelect, onUpdate, onDelete, onDragStart, onDrag, charWidth, charHeight, previewMode, toolMode, parentBandId` + +Uses hooks: `useElementDrag`, `useElementSelection`, `useReportData`, `useBandContext` + +## resolveDBTextValue signature + +```js +resolveDBTextValue(reportData, objectKey, fieldPath, parentBandId, { currentBandData }) +``` +- objectKey = `''` for band-relative field access + +## ExpressionField Field Ref Format + +`[objectKey.fieldPath]` for top-level fields, `[fieldPath]` for band-relative fields. +Strip brackets, split on first `.` to get objectKey and fieldPath. diff --git a/Project-Assignment.filled.docx b/Project-Assignment.filled.docx index 2801a3d..ca2ebd1 100644 Binary files a/Project-Assignment.filled.docx and b/Project-Assignment.filled.docx differ diff --git a/frontend/src/components/ReportEditor/EditorCanvas.jsx b/frontend/src/components/ReportEditor/EditorCanvas.jsx index b560366..f8e8205 100644 --- a/frontend/src/components/ReportEditor/EditorCanvas.jsx +++ b/frontend/src/components/ReportEditor/EditorCanvas.jsx @@ -1,6 +1,7 @@ import React, { useRef, useEffect, useMemo } from 'react'; import TextField from './TextField'; import DBTextField from './DBTextField'; +import ExpressionField from './ExpressionField'; import FrameElement from './FrameElement'; import HorizontalLine from './HorizontalLine'; import VerticalLine from './VerticalLine'; @@ -123,6 +124,19 @@ function EditorCanvas({ fieldPath: '' }; onAddElement(newElement); + } else if (toolMode === 'addExpr') { + const newElement = { + id: `expr-${Date.now()}`, + type: 'expr', + x: col, + y: row, + width: 10, + height: 1, + expression: '', + dataType: 'general', + alignment: 'left' + }; + onAddElement(newElement); } else if (toolMode === 'addFrame') { if (!frameStart) { // First click - set start position @@ -451,6 +465,14 @@ function EditorCanvas({ parentBandId={parentBandId} /> ); + } else if (element.type === 'expr') { + return ( + + ); } else if (element.type === 'frame') { return ; } else if (element.type === 'hline') { diff --git a/frontend/src/components/ReportEditor/ExpressionEditorPanel.jsx b/frontend/src/components/ReportEditor/ExpressionEditorPanel.jsx new file mode 100644 index 0000000..3e211b5 --- /dev/null +++ b/frontend/src/components/ReportEditor/ExpressionEditorPanel.jsx @@ -0,0 +1,116 @@ +import React, { useState, useRef } from 'react'; +import { getAvailableFields, getAvailableObjects } from './utils/dataResolver'; + +function ExpressionEditorPanel({ element, onUpdate, reportData, allElements }) { + const [selectedField, setSelectedField] = useState(''); + const textareaRef = useRef(null); + + const parentBand = allElements.find(el => el.type === 'band' && el.children && el.children.includes(element.id)); + + let availableFields = []; + + if (parentBand && parentBand.dataSource) { + let bandDataArray; + + if (parentBand.parentBandId) { + const grandparentBand = allElements.find(el => el.id === parentBand.parentBandId); + if (grandparentBand && grandparentBand.dataSource) { + const grandparentData = reportData?.[grandparentBand.dataSource]; + if (Array.isArray(grandparentData) && grandparentData.length > 0) { + const nestedData = grandparentData[0][parentBand.dataSource]; + if (Array.isArray(nestedData) && nestedData.length > 0) { + bandDataArray = nestedData; + } + } + } + } else { + bandDataArray = reportData?.[parentBand.dataSource]; + } + + if (Array.isArray(bandDataArray) && bandDataArray.length > 0) { + availableFields = getAvailableFields(bandDataArray[0]); + } + } else { + const objectKeys = getAvailableObjects(reportData); + objectKeys.forEach(objectKey => { + const fields = getAvailableFields(reportData[objectKey]); + fields.forEach(f => availableFields.push(`${objectKey}.${f}`)); + }); + } + + const insertAtCursor = (text) => { + const ta = textareaRef.current; + if (!ta) { onUpdate({ expression: (element.expression || '') + text }); return; } + const start = ta.selectionStart; + const end = ta.selectionEnd; + const current = element.expression || ''; + const next = current.slice(0, start) + text + current.slice(end); + onUpdate({ expression: next }); + requestAnimationFrame(() => { + ta.selectionStart = ta.selectionEnd = start + text.length; + ta.focus(); + }); + }; + + return ( +
+
Expression
+ +