diff --git a/docs/DataGrid Formatting.md b/docs/DataGrid Formatting.md new file mode 100644 index 0000000..79520bb --- /dev/null +++ b/docs/DataGrid Formatting.md @@ -0,0 +1,480 @@ +# DataGrid Formatting + +## Overview + +This document describes the formatting capabilities for the DataGrid component. + +**Formatting applies at three levels:** + +| Level | Cells Targeted | Condition Evaluated On | +|------------|-------------------------|----------------------------------------------| +| **Cell** | 1 specific cell | The cell value | +| **Row** | All cells in the row | Each cell value (or fixed column with `col`) | +| **Column** | All cells in the column | Each cell value (or fixed row with `row`) | + +--- + +## Format Rule Structure + +A format is a **list** of rules. Each rule is an object: + +```json +{ + "condition": {}, + "style": {}, + "formatter": {} +} +``` + +**Rules:** + +- `style` and `formatter` can appear alone (unconditional formatting) +- `condition` **cannot** appear alone - must be paired with `style` and/or `formatter` +- If `condition` is present, the `style`/`formatter` is applied **only if** the condition is met +- Rules are evaluated in order; multiple rules can match + +--- + +## Conflict Resolution + +When multiple rules match the same cell: + +1. **Specificity** = number of conditions in the rule +2. **Higher specificity wins** +3. **At equal specificity, last rule wins entirely** (no fusion) + +```json +[ + { + "style": { + "color": "gray" + } + }, + { + "condition": { + "operator": "<", + "value": 0 + }, + "style": { + "color": "red" + } + }, + { + "condition": { + "operator": "==", + "value": -5 + }, + "style": { + "color": "black" + } + } +] +``` + +For `value = -5`: Rule 3 wins (same specificity as rule 2, but defined later). + +--- + +## Condition Structure + +### Fields + +| Field | Type | Default | Required | Description | +|------------------|------------------------|---------|----------|---------------------------------------------| +| `operator` | string | - | Yes | Comparison operator | +| `value` | scalar / list / object | - | Depends | Value to compare against | +| `not` | bool | `false` | No | Inverts the condition result | +| `case_sensitive` | bool | `false` | No | Case-sensitive string comparison | +| `col` | string | - | No | Reference column (for row-level conditions) | +| `row` | int | - | No | Reference row (for column-level conditions) | + +### Operators + +| Operator | Description | Value Required | +|--------------|--------------------------|------------------| +| `==` | Equal | Yes | +| `!=` | Not equal | Yes | +| `<` | Less than | Yes | +| `<=` | Less than or equal | Yes | +| `>` | Greater than | Yes | +| `>=` | Greater than or equal | Yes | +| `contains` | String contains | Yes | +| `startswith` | String starts with | Yes | +| `endswith` | String ends with | Yes | +| `in` | Value in list | Yes (list) | +| `between` | Value between two values | Yes ([min, max]) | +| `isempty` | Value is empty/null | No | +| `isnotempty` | Value is not empty/null | No | + +### Value Types + +**Literal value:** + +```json +{ + "operator": "<", + "value": 0 +} +{ + "operator": "in", + "value": [ + "A", + "B", + "C" + ] +} +``` + +**Cell reference (compare with another column):** + +```json +{ + "operator": ">", + "value": { + "col": "budget" + } +} +``` + +### Negation + +Use the `not` flag instead of separate operators: + +```json +{ + "operator": "in", + "value": [ + "A", + "B" + ], + "not": true +} +{ + "operator": "contains", + "value": "error", + "not": true +} +``` + +### Case Sensitivity + +String comparisons are **case-insensitive by default**. + +```json +{ + "operator": "==", + "value": "Error", + "case_sensitive": true +} +``` + +### Evaluation Behavior + +| Situation | Behavior | +|---------------------------|-----------------------------------| +| Cell value is `null` | Condition = `false` | +| Referenced cell is `null` | Condition = `false` | +| Type mismatch | Condition = `false` (no coercion) | +| String operators | Converts value to string first | + +### Examples + +```json +// Row-level: highlight if "status" column == "error" +{ + "col": "status", + "operator": "==", + "value": "error" +} + +// Column-level: bold if row 0 has value "Total" +{ + "row": 0, + "operator": "==", + "value": "Total" +} + +// Compare with another column +{ + "operator": ">", + "value": { + "col": "budget" + } +} + +// Negated condition +{ + "operator": "in", + "value": [ + "draft", + "pending" + ], + "not": true +} +``` + +--- + +## Style Structure + +### Fields + +| Field | Type | Default | Description | +|--------------------|--------|------------|---------------------------------------------------| +| `preset` | string | - | Preset name (applied first, can be overridden) | +| `background_color` | string | - | Background color (hex, CSS name, or CSS variable) | +| `color` | string | - | Text color | +| `font_weight` | string | `"normal"` | `"normal"` or `"bold"` | +| `font_style` | string | `"normal"` | `"normal"` or `"italic"` | +| `font_size` | string | - | Font size (`"12px"`, `"0.9em"`) | +| `text_decoration` | string | `"none"` | `"none"`, `"underline"`, `"line-through"` | + +### Example + +```json +{ + "style": { + "preset": "success", + "font_weight": "bold" + } +} +``` + +### Default Presets (DaisyUI 5) + +| Preset | Background | Text | +|-------------|--------------------------|----------------------------------| +| `primary` | `var(--color-primary)` | `var(--color-primary-content)` | +| `secondary` | `var(--color-secondary)` | `var(--color-secondary-content)` | +| `accent` | `var(--color-accent)` | `var(--color-accent-content)` | +| `neutral` | `var(--color-neutral)` | `var(--color-neutral-content)` | +| `info` | `var(--color-info)` | `var(--color-info-content)` | +| `success` | `var(--color-success)` | `var(--color-success-content)` | +| `warning` | `var(--color-warning)` | `var(--color-warning-content)` | +| `error` | `var(--color-error)` | `var(--color-error-content)` | + +All presets default to `font_weight: "normal"`, `font_style: "normal"`, `text_decoration: "none"`. + +### Resolution Logic + +1. If `preset` is specified, apply all preset properties +2. Override with any explicit properties + +**No style fusion:** When multiple rules match, the winning rule's style applies entirely. + +--- + +## Formatter Structure + +Formatters transform cell values for display without changing the underlying data. + +### Usage + +```json +{ + "formatter": { + "preset": "EUR" + } +} +{ + "formatter": { + "preset": "EUR", + "precision": 3 + } +} +``` + +### Error Handling + +If formatting fails (e.g., non-numeric value for `number` formatter), display `"⚠"`. + +--- + +## Formatter Types + +### `number` + +For numbers, currencies, and percentages. + +| Property | Type | Default | Description | +|-----------------|--------|---------|-------------------------------| +| `prefix` | string | `""` | Text before value | +| `suffix` | string | `""` | Text after value | +| `thousands_sep` | string | `""` | Thousands separator | +| `decimal_sep` | string | `"."` | Decimal separator | +| `precision` | int | `0` | Number of decimal places | +| `multiplier` | number | `1` | Multiply value before display | + +### `date` + +For dates and datetimes. + +| Property | Type | Default | Description | +|----------|--------|--------------|-------------------------| +| `format` | string | `"%Y-%m-%d"` | strftime format pattern | + +### `boolean` + +For true/false values. + +| Property | Type | Default | Description | +|---------------|--------|-----------|-------------------| +| `true_value` | string | `"true"` | Display for true | +| `false_value` | string | `"false"` | Display for false | +| `null_value` | string | `""` | Display for null | + +### `text` + +For text transformations. + +| Property | Type | Default | Description | +|--------------|--------|---------|----------------------------------------------| +| `transform` | string | - | `"uppercase"`, `"lowercase"`, `"capitalize"` | +| `max_length` | int | - | Truncate if exceeded | +| `ellipsis` | string | `"..."` | Suffix when truncated | + +### `enum` + +For mapping values to display labels. Also used for Select dropdowns. + +| Property | Type | Default | Description | +|---------------|--------|------------------|------------------------------------| +| `source` | object | - | Data source (see below) | +| `default` | string | `""` | Label for unknown values | +| `allow_empty` | bool | `true` | Show empty option in Select | +| `empty_label` | string | `"-- Select --"` | Label for empty option | +| `order_by` | string | `"source"` | `"source"`, `"display"`, `"value"` | + +#### Source Types + +**Static mapping:** + +```json +{ + "type": "enum", + "source": { + "type": "mapping", + "value": { + "draft": "Brouillon", + "pending": "En attente", + "approved": "Approuvé" + } + }, + "default": "Inconnu" +} +``` + +**From another DataGrid:** + +```json +{ + "type": "enum", + "source": { + "type": "datagrid", + "value": "categories_grid", + "value_column": "id", + "display_column": "name" + } +} +``` + +#### Empty Value Behavior + +- `allow_empty: true` → Empty option displayed with `empty_label` +- `allow_empty: false` → First entry selected by default + +--- + +## Default Formatter Presets + +```python +formatter_presets = { + "EUR": { + "type": "number", + "suffix": " €", + "thousands_sep": " ", + "decimal_sep": ",", + "precision": 2 + }, + "USD": { + "type": "number", + "prefix": "$", + "thousands_sep": ",", + "decimal_sep": ".", + "precision": 2 + }, + "percentage": { + "type": "number", + "suffix": "%", + "precision": 1, + "multiplier": 100 + }, + "short_date": { + "type": "date", + "format": "%d/%m/%Y" + }, + "iso_date": { + "type": "date", + "format": "%Y-%m-%d" + }, + "yes_no": { + "type": "boolean", + "true_value": "Yes", + "false_value": "No" + } +} +``` + +--- + +## Storage Architecture + +### Format Storage Location + +| Level | Storage | Key | +|------------|------------------------------|---------| +| **Column** | `DataGridColumnState.format` | - | +| **Row** | `DataGridRowState.format` | - | +| **Cell** | `DatagridState.cell_formats` | Cell ID | + +### Cell ID Format + +``` +tcell_{datagrid_id}-{row_index}-{col_index} +``` + +--- + +## DataGridsManager + +Global settings stored in `DataGridsManager`: + +| Property | Type | Description | +|---------------------|--------|-------------------------------------------| +| `style_presets` | dict | Style presets (primary, success, etc.) | +| `formatter_presets` | dict | Formatter presets (EUR, percentage, etc.) | +| `default_locale` | string | Default locale for number/date formatting | + +--- + +## Future Considerations + +- **AND/OR conditions**: Add explicit `and`/`or` operators if `between`/`in` prove insufficient +- **Cell references**: Extend to `{"col": "x", "row": 0}` for specific cell and `{"col": "x", "row_offset": -1}` for + relative references +- **Enum cascade (draft)**: Dependent dropdowns with `depends_on` and `filter_column` + ```json + { + "source": { + "type": "datagrid", + "value": "cities_grid", + "value_column": "id", + "display_column": "name", + "filter_column": "country_id" + }, + "depends_on": "country" + } + ``` +- **API source for enum**: `{"type": "api", "value": "https://...", ...}` +- **Searchable enum**: For large option lists +- **Formatter chaining**: Apply multiple formatters in sequence diff --git a/src/myfasthtml/controls/DataGrid.py b/src/myfasthtml/controls/DataGrid.py index 14efe7f..acbd91a 100644 --- a/src/myfasthtml/controls/DataGrid.py +++ b/src/myfasthtml/controls/DataGrid.py @@ -62,6 +62,7 @@ class DatagridState(DbObject): self.filtered: dict = {} self.edition: DatagridEditionState = DatagridEditionState() self.selection: DatagridSelectionState = DatagridSelectionState() + self.cell_formats: dict = {} self.ne_df = None self.ns_fast_access = None @@ -489,9 +490,6 @@ class DataGrid(MultipleInstance): OPTIMIZED: Accepts pre-computed filter_keyword_lower to avoid repeated dict lookups. OPTIMIZED: Uses OptimizedDiv instead of Div for faster rendering. """ - if not col_def.usable: - return None - if not col_def.visible: return None @@ -652,9 +650,6 @@ class DataGrid(MultipleInstance): appropriate default content or styling is applied. :rtype: Div | None """ - if not col_def.usable: - return None - if not col_def.visible: return Div(cls="dt2-col-hidden") diff --git a/src/myfasthtml/controls/datagrid_objects.py b/src/myfasthtml/controls/datagrid_objects.py index 43ce23b..b02623e 100644 --- a/src/myfasthtml/controls/datagrid_objects.py +++ b/src/myfasthtml/controls/datagrid_objects.py @@ -8,6 +8,7 @@ class DataGridRowState: row_id: int visible: bool = True height: int | None = None + format: list = field(default_factory=list) @dataclass @@ -17,8 +18,8 @@ class DataGridColumnState: title: str = None type: ColumnType = ColumnType.Text visible: bool = True - usable: bool = True width: int = DATAGRID_DEFAULT_COLUMN_WIDTH + format: list = field(default_factory=list) # @dataclass diff --git a/src/myfasthtml/controls/helpers.py b/src/myfasthtml/controls/helpers.py index a410dcc..c8d1771 100644 --- a/src/myfasthtml/controls/helpers.py +++ b/src/myfasthtml/controls/helpers.py @@ -159,5 +159,5 @@ icons = { ColumnType.Number: number_row20_regular, ColumnType.Datetime: calendar_ltr20_regular, ColumnType.Bool: checkbox_checked20_filled, - ColumnType.List: text_bullet_list_square20_regular, + ColumnType.Enum: text_bullet_list_square20_regular, } diff --git a/src/myfasthtml/core/commands.py b/src/myfasthtml/core/commands.py index 236871d..415db85 100644 --- a/src/myfasthtml/core/commands.py +++ b/src/myfasthtml/core/commands.py @@ -121,7 +121,7 @@ class Command: if escaped: res["hx-vals"] = html.escape(json.dumps(res["hx-vals"])) - if values_encode is "json": + if values_encode == "json": res["hx-vals"] = json.dumps(res["hx-vals"]) return res diff --git a/src/myfasthtml/core/constants.py b/src/myfasthtml/core/constants.py index be32113..bca860f 100644 --- a/src/myfasthtml/core/constants.py +++ b/src/myfasthtml/core/constants.py @@ -23,7 +23,7 @@ class ColumnType(Enum): Datetime = "DateTime" Bool = "Boolean" Choice = "Choice" - List = "List" + Enum = "Enum" class ViewType(Enum):