Spaces:
Paused
Paused
| """ | |
| Spreadsheet Display Component | |
| Renders tabular data with support for row-based or cell-based annotation. | |
| Usage: | |
| In instance_display config: | |
| fields: | |
| - key: data_table | |
| type: spreadsheet | |
| display_options: | |
| annotation_mode: row | |
| show_headers: true | |
| max_height: 400 | |
| """ | |
| from typing import Dict, Any, List, Optional | |
| import html | |
| import logging | |
| from .base import BaseDisplay | |
| logger = logging.getLogger(__name__) | |
| class SpreadsheetDisplay(BaseDisplay): | |
| """ | |
| Display type for tabular/spreadsheet data. | |
| Renders data as an HTML table with support for row-based | |
| or cell-based annotation modes. | |
| """ | |
| name = "spreadsheet" | |
| required_fields = ["key"] | |
| optional_fields = { | |
| "annotation_mode": "row", # "row", "cell", or "range" | |
| "show_headers": True, # Show column headers | |
| "max_height": 400, # Max container height | |
| "max_width": None, # Max container width | |
| "striped": True, # Alternating row colors | |
| "hoverable": True, # Highlight row on hover | |
| "sortable": False, # Enable column sorting | |
| "filterable": False, # Enable column filtering | |
| "selectable": True, # Enable row/cell selection | |
| "compact": False, # Compact table styling | |
| # New styling options | |
| "border_style": "default", # "default", "bordered", "minimal", "rounded", "none" | |
| "header_style": "default", # "default", "dark", "primary", "gradient", "light", "transparent" | |
| "custom_class": None, # Additional CSS classes for the table | |
| "custom_css": None, # Inline CSS styles for the table | |
| } | |
| description = "Spreadsheet/table display with row or cell annotation" | |
| supports_span_target = False # No .text-content wrapper; table data not compatible with span offsets | |
| # Valid values for style options | |
| VALID_BORDER_STYLES = ["default", "bordered", "minimal", "rounded", "none"] | |
| VALID_HEADER_STYLES = ["default", "dark", "primary", "gradient", "light", "transparent"] | |
| def validate_config(self, field_config: Dict[str, Any]) -> List[str]: | |
| """ | |
| Validate spreadsheet display configuration. | |
| Returns: | |
| List of validation error messages (empty if valid) | |
| """ | |
| errors = super().validate_config(field_config) | |
| options = field_config.get("display_options", {}) | |
| # Validate border_style | |
| border_style = options.get("border_style", "default") | |
| if border_style not in self.VALID_BORDER_STYLES: | |
| errors.append( | |
| f"Invalid border_style '{border_style}'. " | |
| f"Must be one of: {', '.join(self.VALID_BORDER_STYLES)}" | |
| ) | |
| # Validate header_style | |
| header_style = options.get("header_style", "default") | |
| if header_style not in self.VALID_HEADER_STYLES: | |
| errors.append( | |
| f"Invalid header_style '{header_style}'. " | |
| f"Must be one of: {', '.join(self.VALID_HEADER_STYLES)}" | |
| ) | |
| # Validate annotation_mode | |
| annotation_mode = options.get("annotation_mode", "row") | |
| valid_modes = ["row", "cell", "range"] | |
| if annotation_mode not in valid_modes: | |
| errors.append( | |
| f"Invalid annotation_mode '{annotation_mode}'. " | |
| f"Must be one of: {', '.join(valid_modes)}" | |
| ) | |
| return errors | |
| def render(self, field_config: Dict[str, Any], data: Any) -> str: | |
| """ | |
| Render spreadsheet data. | |
| Args: | |
| field_config: Display configuration | |
| data: Either a dict with extracted content, list of lists, | |
| or list of dicts | |
| Returns: | |
| HTML string for rendering | |
| """ | |
| options = self.get_display_options(field_config) | |
| field_key = field_config.get("key", "spreadsheet") | |
| # Handle different data formats | |
| if isinstance(data, dict): | |
| # Pre-extracted FormatOutput data | |
| if "rendered_html" in data: | |
| return self._wrap_content(data["rendered_html"], options, field_key) | |
| # Extract from metadata | |
| rows = data.get("rows", []) | |
| headers = data.get("headers", data.get("metadata", {}).get("headers", [])) | |
| elif isinstance(data, list): | |
| if data and isinstance(data[0], dict): | |
| # List of dictionaries | |
| headers = list(data[0].keys()) if data else [] | |
| rows = [[row.get(h, "") for h in headers] for row in data] | |
| else: | |
| # List of lists | |
| rows = data | |
| headers = [] | |
| else: | |
| return f'<div class="spreadsheet-error">Unsupported data format</div>' | |
| # Generate table HTML | |
| table_html = self._render_table(rows, headers, options, field_key) | |
| return self._wrap_content(table_html, options, field_key) | |
| def _wrap_content( | |
| self, | |
| content: str, | |
| options: Dict[str, Any], | |
| field_key: str | |
| ) -> str: | |
| """ | |
| Wrap table content in container with styles. | |
| """ | |
| styles = [] | |
| max_height = options.get("max_height") | |
| max_width = options.get("max_width") | |
| if max_height: | |
| styles.append(f"max-height: {max_height}px") | |
| styles.append("overflow-y: auto") | |
| if max_width: | |
| styles.append(f"max-width: {max_width}px") | |
| styles.append("overflow-x: auto") | |
| style_str = "; ".join(styles) if styles else "" | |
| mode = options.get("annotation_mode", "row") | |
| # Container classes | |
| container_classes = ["spreadsheet-display"] | |
| # Add border-rounded class to container for rounded style | |
| border_style = options.get("border_style", "default") | |
| if border_style == "rounded": | |
| container_classes.append("border-rounded") | |
| container_class_str = " ".join(container_classes) | |
| return f''' | |
| <div class="{container_class_str}" | |
| data-field-key="{field_key}" | |
| data-annotation-mode="{mode}" | |
| style="{style_str}"> | |
| {content} | |
| </div> | |
| ''' | |
| def _render_table( | |
| self, | |
| rows: List[List], | |
| headers: List[str], | |
| options: Dict[str, Any], | |
| field_key: str | |
| ) -> str: | |
| """ | |
| Render data as HTML table. | |
| """ | |
| parts = [] | |
| mode = options.get("annotation_mode", "row") | |
| # Table classes | |
| table_classes = ["spreadsheet-table"] | |
| if options.get("striped"): | |
| table_classes.append("table-striped") | |
| if options.get("hoverable"): | |
| table_classes.append("table-hoverable") | |
| if options.get("compact"): | |
| table_classes.append("table-compact") | |
| if options.get("selectable"): | |
| table_classes.append("table-selectable") | |
| # Border style class | |
| border_style = options.get("border_style", "default") | |
| if border_style and border_style != "default": | |
| table_classes.append(f"border-{border_style}") | |
| # Header style class | |
| header_style = options.get("header_style", "default") | |
| if header_style and header_style != "default": | |
| table_classes.append(f"header-{header_style}") | |
| # Custom class from admin config | |
| custom_class = options.get("custom_class") | |
| if custom_class: | |
| # Support both string and list of classes | |
| if isinstance(custom_class, list): | |
| table_classes.extend(custom_class) | |
| else: | |
| table_classes.append(custom_class) | |
| class_str = " ".join(table_classes) | |
| # Custom inline CSS | |
| custom_css = options.get("custom_css", "") | |
| style_attr = f' style="{html.escape(custom_css)}"' if custom_css else "" | |
| parts.append(f'<table class="{class_str}" data-mode="{mode}"{style_attr}>') | |
| # Headers | |
| if headers and options.get("show_headers", True): | |
| parts.append('<thead><tr>') | |
| if options.get("selectable") and mode == "row": | |
| parts.append('<th class="select-col"></th>') | |
| for col_idx, header in enumerate(headers): | |
| sortable_attr = 'data-sortable="true"' if options.get("sortable") else "" | |
| parts.append( | |
| f'<th data-col="{col_idx}" {sortable_attr}>' | |
| f'{html.escape(str(header))}</th>' | |
| ) | |
| parts.append('</tr></thead>') | |
| # Body | |
| parts.append('<tbody>') | |
| for row_idx, row in enumerate(rows): | |
| row_classes = ["spreadsheet-row"] | |
| if mode == "row": | |
| row_classes.append("selectable-row") | |
| parts.append( | |
| f'<tr class="{" ".join(row_classes)}" ' | |
| f'data-row="{row_idx}">' | |
| ) | |
| # Row selection checkbox | |
| if options.get("selectable") and mode == "row": | |
| parts.append( | |
| f'<td class="select-col">' | |
| f'<input type="checkbox" class="row-select" data-row="{row_idx}">' | |
| f'</td>' | |
| ) | |
| for col_idx, cell in enumerate(row): | |
| cell_value = str(cell) if cell is not None else "" | |
| cell_classes = ["spreadsheet-cell"] | |
| if mode == "cell": | |
| cell_classes.append("selectable-cell") | |
| cell_ref = self._get_cell_ref(row_idx, col_idx) | |
| parts.append( | |
| f'<td class="{" ".join(cell_classes)}" ' | |
| f'data-row="{row_idx}" ' | |
| f'data-col="{col_idx}" ' | |
| f'data-cell-ref="{cell_ref}">' | |
| f'{html.escape(cell_value)}' | |
| f'</td>' | |
| ) | |
| parts.append('</tr>') | |
| parts.append('</tbody>') | |
| parts.append('</table>') | |
| # Add selection summary for row mode | |
| if options.get("selectable") and mode == "row": | |
| parts.append(''' | |
| <div class="spreadsheet-selection-summary"> | |
| <span class="selected-count">0</span> rows selected | |
| </div> | |
| ''') | |
| return "\n".join(parts) | |
| def _get_cell_ref(self, row: int, col: int) -> str: | |
| """ | |
| Get A1-style cell reference. | |
| """ | |
| # Convert column to letter | |
| col_letter = "" | |
| col_num = col + 1 | |
| while col_num > 0: | |
| col_num, remainder = divmod(col_num - 1, 26) | |
| col_letter = chr(65 + remainder) + col_letter | |
| return f"{col_letter}{row + 1}" | |
| def get_css_classes(self, field_config: Dict[str, Any]) -> List[str]: | |
| """Get CSS classes for the display container.""" | |
| classes = super().get_css_classes(field_config) | |
| options = self.get_display_options(field_config) | |
| if field_config.get("span_target"): | |
| classes.append("span-target-spreadsheet") | |
| mode = options.get("annotation_mode", "row") | |
| classes.append(f"spreadsheet-mode-{mode}") | |
| return classes | |
| def get_data_attributes( | |
| self, | |
| field_config: Dict[str, Any], | |
| data: Any | |
| ) -> Dict[str, str]: | |
| """Get data attributes for JavaScript initialization.""" | |
| attrs = super().get_data_attributes(field_config, data) | |
| options = self.get_display_options(field_config) | |
| attrs["annotation-mode"] = options.get("annotation_mode", "row") | |
| attrs["selectable"] = str(options.get("selectable", True)).lower() | |
| return attrs | |
| def get_js_init(self) -> Optional[str]: | |
| """ | |
| Return JavaScript initialization code for spreadsheet interactivity. | |
| """ | |
| return ''' | |
| if (typeof initSpreadsheetDisplays === 'function') { | |
| initSpreadsheetDisplays(); | |
| } | |
| ''' | |