report editor has db text and bands (detail, subdetails, summary, header, footer) detail/subdetail work ok.
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* Utilities for rendering bands in preview mode
|
||||
* Handles band iteration, nesting, and layout calculation
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolve nested path in object (e.g., 'contact.phone')
|
||||
*/
|
||||
function resolveNestedPath(obj, path) {
|
||||
if (!path || !obj) return undefined;
|
||||
|
||||
const parts = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
for (const part of parts) {
|
||||
if (current && typeof current === 'object' && part in current) {
|
||||
current = current[part];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data array for a band
|
||||
* @param {Object} reportData - The full report data
|
||||
* @param {string} dataSource - Path to array in reportData (e.g., 'measurements', 'items')
|
||||
* @param {Object} parentBandData - Data from parent band (for nested bands)
|
||||
* @returns {Array} - Array of data items for band iteration
|
||||
*/
|
||||
export function getBandDataArray(reportData, dataSource, parentBandData = null) {
|
||||
if (!dataSource) return [];
|
||||
|
||||
// If inside parent band, use parent's current data
|
||||
if (parentBandData) {
|
||||
const data = resolveNestedPath(parentBandData, dataSource);
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
// Otherwise, use reportData
|
||||
if (!reportData) return [];
|
||||
const data = reportData[dataSource];
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate band instances for preview mode
|
||||
* Returns array of band instances with positions and data
|
||||
* @param {Array} bands - Array of band elements
|
||||
* @param {Object} reportData - The full report data
|
||||
* @param {Array} allElements - All elements in the report
|
||||
* @param {Object} parentBandData - Data from parent band (for nested bands)
|
||||
* @param {number} yOffset - Y offset for nested bands (parent instance's Y position)
|
||||
* @param {number} parentDesignY - Design Y position of parent band (for relative positioning)
|
||||
* @returns {Array} - Array of band instances
|
||||
*/
|
||||
export function calculateBandInstances(bands, reportData, allElements, parentBandData = null, yOffset = null, parentDesignY = null) {
|
||||
const instances = [];
|
||||
|
||||
// Get bands at current nesting level
|
||||
const currentLevelBands = bands.filter(band =>
|
||||
parentBandData ? band.parentBandId === parentBandData.parentBandId : !band.parentBandId
|
||||
);
|
||||
|
||||
// Sort bands by Y position
|
||||
const sortedBands = [...currentLevelBands].sort((a, b) => a.y - b.y);
|
||||
|
||||
// Track where previous band ended in both design and preview mode
|
||||
let previousBandDesignEnd = null;
|
||||
let previousBandPreviewEnd = null;
|
||||
let currentY = 0;
|
||||
|
||||
for (const band of sortedBands) {
|
||||
// Calculate starting Y for this band
|
||||
if (previousBandDesignEnd === null) {
|
||||
// First band at this nesting level
|
||||
if (yOffset !== null && parentDesignY !== null) {
|
||||
// Nested band: position relative to parent
|
||||
const relativeY = band.y - parentDesignY;
|
||||
currentY = yOffset + relativeY;
|
||||
} else {
|
||||
// Top-level band: use design Y
|
||||
currentY = band.y;
|
||||
}
|
||||
} else {
|
||||
// Subsequent bands: maintain the gap between bands from design mode
|
||||
const designGap = band.y - previousBandDesignEnd - 1;
|
||||
// Start this band after the previous band's preview end + the design gap
|
||||
currentY = previousBandPreviewEnd + 1 + designGap;
|
||||
}
|
||||
|
||||
const bandStartY = currentY;
|
||||
const dataArray = getBandDataArray(reportData, band.dataSource, parentBandData);
|
||||
|
||||
// Get child elements for this band
|
||||
const childElements = allElements.filter(el => band.children && band.children.includes(el.id));
|
||||
|
||||
// Get nested bands (bands that have this band as parent)
|
||||
const nestedBands = bands.filter(b => b.parentBandId === band.id);
|
||||
|
||||
if (dataArray.length === 0) {
|
||||
// No data - render once with null data (for static bands like headers/footers)
|
||||
const bandInstance = {
|
||||
bandId: band.id,
|
||||
instanceIndex: 0,
|
||||
y: currentY,
|
||||
height: band.height,
|
||||
data: null,
|
||||
childElements: childElements,
|
||||
originalBandY: band.y,
|
||||
bandType: band.bandType
|
||||
};
|
||||
|
||||
instances.push(bandInstance);
|
||||
|
||||
// Handle nested bands with no data
|
||||
if (nestedBands.length > 0) {
|
||||
const nestedInstances = calculateBandInstances(
|
||||
nestedBands,
|
||||
reportData,
|
||||
allElements,
|
||||
null, // No parent data
|
||||
currentY, // Parent instance Y
|
||||
band.y // Parent design Y
|
||||
);
|
||||
instances.push(...nestedInstances);
|
||||
currentY = Math.max(currentY + band.height, ...nestedInstances.map(ni => ni.y + ni.height));
|
||||
} else {
|
||||
currentY += band.height;
|
||||
}
|
||||
} else {
|
||||
// Render once per data item
|
||||
dataArray.forEach((item, index) => {
|
||||
const bandInstance = {
|
||||
bandId: band.id,
|
||||
instanceIndex: index,
|
||||
y: currentY,
|
||||
height: band.height,
|
||||
data: item,
|
||||
childElements: childElements,
|
||||
originalBandY: band.y,
|
||||
bandType: band.bandType
|
||||
};
|
||||
|
||||
instances.push(bandInstance);
|
||||
|
||||
// Handle nested bands
|
||||
if (nestedBands.length > 0) {
|
||||
const nestedInstances = calculateBandInstances(
|
||||
nestedBands,
|
||||
reportData,
|
||||
allElements,
|
||||
{ ...item, parentBandId: band.id }, // Pass current item as parent data
|
||||
currentY, // Parent instance Y
|
||||
band.y // Parent design Y
|
||||
);
|
||||
instances.push(...nestedInstances);
|
||||
// Update currentY to account for nested band height
|
||||
const nestedHeight = nestedInstances.reduce((max, ni) => Math.max(max, ni.y + ni.height), currentY + band.height);
|
||||
currentY = nestedHeight;
|
||||
} else {
|
||||
currentY += band.height;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Track where this band ended in both design and preview mode
|
||||
previousBandDesignEnd = band.y + band.height - 1;
|
||||
previousBandPreviewEnd = currentY - 1;
|
||||
}
|
||||
|
||||
return instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total content height for preview mode
|
||||
* @param {Array} bands - Array of band elements
|
||||
* @param {Object} reportData - The full report data
|
||||
* @param {Array} allElements - All elements in the report
|
||||
* @param {number} minHeight - Minimum height (default canvas height)
|
||||
* @returns {number} - Total height in grid rows
|
||||
*/
|
||||
export function calculateTotalContentHeight(bands, reportData, allElements, minHeight = 66) {
|
||||
if (bands.length === 0) {
|
||||
return minHeight;
|
||||
}
|
||||
|
||||
const instances = calculateBandInstances(bands, reportData, allElements);
|
||||
|
||||
if (instances.length === 0) {
|
||||
return minHeight;
|
||||
}
|
||||
|
||||
// Find the maximum Y + height from all instances
|
||||
const maxY = instances.reduce((max, instance) => {
|
||||
return Math.max(max, instance.y + instance.height);
|
||||
}, 0);
|
||||
|
||||
// Also consider non-band elements
|
||||
const nonBandElements = allElements.filter(el => el.type !== 'band' && !bands.some(b => b.children.includes(el.id)));
|
||||
const maxElementY = nonBandElements.reduce((max, el) => {
|
||||
const bounds = el.getBounds ? el.getBounds() : { maxY: el.y };
|
||||
return Math.max(max, bounds.maxY + 1);
|
||||
}, 0);
|
||||
|
||||
return Math.max(minHeight, maxY, maxElementY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available arrays from reportData (for ObjectInspector dropdown)
|
||||
* @param {Object} reportData - The full report data
|
||||
* @param {Object} parentBand - Parent band (for nested bands)
|
||||
* @returns {Array} - Array of { key, path } objects
|
||||
*/
|
||||
export function getAvailableArrays(reportData, parentBand = null) {
|
||||
const arrays = [];
|
||||
|
||||
// If there's a parent band, get arrays from the parent's data structure
|
||||
if (parentBand && parentBand.dataSource) {
|
||||
const parentDataArray = reportData?.[parentBand.dataSource];
|
||||
if (Array.isArray(parentDataArray) && parentDataArray.length > 0) {
|
||||
// Look at first item in parent's array to find nested arrays
|
||||
const sampleItem = parentDataArray[0];
|
||||
findArraysInObject(sampleItem, arrays);
|
||||
return arrays;
|
||||
}
|
||||
return []; // Parent has no data
|
||||
}
|
||||
|
||||
// No parent band - get top-level arrays only
|
||||
function findTopLevelArrays(obj) {
|
||||
if (!obj || typeof obj !== 'object') return;
|
||||
|
||||
for (const key of Object.keys(obj)) {
|
||||
const value = obj[key];
|
||||
if (Array.isArray(value)) {
|
||||
arrays.push({ key, path: key });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findTopLevelArrays(reportData);
|
||||
return arrays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to find arrays within an object (for nested band data sources)
|
||||
* @param {Object} obj - Object to search
|
||||
* @param {Array} arrays - Array to populate with results
|
||||
* @param {string} prefix - Path prefix for nested objects
|
||||
*/
|
||||
function findArraysInObject(obj, arrays, prefix = '') {
|
||||
if (!obj || typeof obj !== 'object') return;
|
||||
|
||||
for (const key of Object.keys(obj)) {
|
||||
const value = obj[key];
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
arrays.push({ key, path });
|
||||
// Don't recurse into arrays - we only want immediate arrays
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
// Recurse into nested objects
|
||||
findArraysInObject(value, arrays, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Resolve nested path in object (e.g., 'contact.phone')
|
||||
*/
|
||||
function resolveNestedPath(obj, path) {
|
||||
if (!path || !obj) return undefined;
|
||||
|
||||
const parts = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
for (const part of parts) {
|
||||
if (current && typeof current === 'object' && part in current) {
|
||||
current = current[part];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve DBTextField value from reportData
|
||||
* @param {Object} reportData - The full report data
|
||||
* @param {string} objectKey - Top-level object key (e.g., 'owner', 'vessel')
|
||||
* @param {string} fieldPath - Dot-notation path to field (e.g., 'name', 'contact.phone')
|
||||
* @param {string} parentBandId - ID of parent band (if element is inside a band)
|
||||
* @param {Object} bandContext - Band context with currentBandData map
|
||||
* @returns {string} - Resolved value as string
|
||||
*/
|
||||
export function resolveDBTextValue(reportData, objectKey, fieldPath, parentBandId = null, bandContext = null) {
|
||||
// If inside band, use band's current data for automatic binding
|
||||
if (parentBandId && bandContext && bandContext.currentBandData && bandContext.currentBandData[parentBandId]) {
|
||||
const bandData = bandContext.currentBandData[parentBandId];
|
||||
|
||||
// If objectKey is empty, resolve directly from band data
|
||||
if (!objectKey) {
|
||||
if (!fieldPath) return '';
|
||||
const value = resolveNestedPath(bandData, fieldPath);
|
||||
return value !== undefined ? String(value) : '';
|
||||
}
|
||||
|
||||
// If objectKey is provided, resolve from objectKey in band data
|
||||
const objectData = bandData[objectKey];
|
||||
if (!objectData) return '';
|
||||
|
||||
if (!fieldPath) {
|
||||
return typeof objectData === 'object' ? '' : String(objectData);
|
||||
}
|
||||
|
||||
const value = resolveNestedPath(objectData, fieldPath);
|
||||
return value !== undefined ? String(value) : '';
|
||||
}
|
||||
|
||||
// Normal resolution (not in band or no band context)
|
||||
if (!reportData || !objectKey) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const objectData = reportData[objectKey];
|
||||
if (!objectData) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!fieldPath) {
|
||||
// If no fieldPath, try to display the object itself (if primitive)
|
||||
return typeof objectData === 'object' ? '' : String(objectData);
|
||||
}
|
||||
|
||||
const value = resolveNestedPath(objectData, fieldPath);
|
||||
return value !== undefined ? String(value) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available object keys from reportData
|
||||
*/
|
||||
export function getAvailableObjects(reportData) {
|
||||
if (!reportData || typeof reportData !== 'object') {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(reportData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available field paths from an object (recursive)
|
||||
*/
|
||||
export function getAvailableFields(obj, prefix = '') {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fields = [];
|
||||
|
||||
for (const key of Object.keys(obj)) {
|
||||
const fullPath = prefix ? `${prefix}.${key}` : key;
|
||||
const value = obj[key];
|
||||
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
// Nested object - add this key and recurse
|
||||
fields.push(fullPath);
|
||||
fields.push(...getAvailableFields(value, fullPath));
|
||||
} else {
|
||||
// Primitive or array - add this key
|
||||
fields.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Element } from '../models/Element';
|
||||
|
||||
/**
|
||||
* Create an element instance from plain object data
|
||||
* This allows us to use class methods on plain state objects
|
||||
*/
|
||||
export function createElementInstance(elementData) {
|
||||
return Element.fromJSON(elementData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element style using class method
|
||||
*/
|
||||
export function getElementStyle(element, charWidth, charHeight) {
|
||||
const instance = createElementInstance(element);
|
||||
return instance.getStyle(charWidth, charHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element bounds using class method
|
||||
*/
|
||||
export function getElementBounds(element) {
|
||||
const instance = createElementInstance(element);
|
||||
return instance.getBounds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element dimensions using class method
|
||||
*/
|
||||
export function getElementDimensions(element) {
|
||||
const instance = createElementInstance(element);
|
||||
return instance.getDimensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if element intersects with rectangle
|
||||
*/
|
||||
export function elementIntersectsRect(element, rect) {
|
||||
const instance = createElementInstance(element);
|
||||
return instance.intersectsRect(rect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if text element is multi-line
|
||||
*/
|
||||
export function isTextMultiLine(element) {
|
||||
if (element.type !== 'text') return false;
|
||||
const instance = createElementInstance(element);
|
||||
return instance.isMultiLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get white-space style for text element
|
||||
*/
|
||||
export function getTextWhiteSpace(element) {
|
||||
if (element.type !== 'text') return 'normal';
|
||||
const instance = createElementInstance(element);
|
||||
return instance.getWhiteSpace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if position is on frame border
|
||||
*/
|
||||
export function isPositionOnFrameBorder(element, relativeCol, relativeRow) {
|
||||
if (element.type !== 'frame') return false;
|
||||
const instance = createElementInstance(element);
|
||||
return instance.isPositionOnBorder(relativeCol, relativeRow);
|
||||
}
|
||||
Reference in New Issue
Block a user