diff --git a/.gitignore b/.gitignore index 8a30ffa..b47db47 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ types.d.ts /frontend/index.html vite.generated.ts vite.config.ts -/src/main/dev-bundle \ No newline at end of file +/src/main/dev-bundle +/.claude/settings.local.json \ No newline at end of file diff --git a/CONFIGURATION_RESOLUTION.md b/CONFIGURATION_RESOLUTION.md new file mode 100644 index 0000000..918fcc4 --- /dev/null +++ b/CONFIGURATION_RESOLUTION.md @@ -0,0 +1,106 @@ +# Configuration Resolution: Scope-First vs. Type-First + +## The Two Strategies + +Given `Foo extends Entity` with configurations at four points in the matrix: + +| | Instance | Global | +|---|---|---| +| **Foo** | I·Foo | G·Foo | +| **Entity** | I·Entity | G·Entity | + +**Scope-first** exhausts all instance settings before looking at global ones: +``` +I·Foo → I·Entity → G·Foo → G·Entity → Default +``` + +**Type-first** exhausts all Foo-specific settings before looking at Entity: +``` +I·Foo → G·Foo → I·Entity → G·Entity → Default +``` + +Both agree on `I·Foo` being first and `G·Entity` being last. The dispute is the middle two. + +--- + +## Where Scope-First Makes Sense + +**Setting: `textAlign` or `nullRepresentation`** + +A developer creates a reporting grid and writes: +```java +easyGrid.typeConfiguration(Entity.class).setTextAlign(RIGHT); +``` +The intent is *"every Entity-like column in this grid should be right-aligned."* Since `Foo IS-AN Entity`, Foo columns should also be right-aligned. Instance context wins: the developer explicitly opted every Entity column into this layout decision, and Foo, being substitutable for Entity, should inherit it. + +If type-first were applied, `G·Foo` would sit between `I·Foo` and `I·Entity`. An unrelated global renderer for Foo would silently block the instance-level alignment — which is the opposite of what the developer intended. + +--- + +## Where Type-First Makes Sense + +**Setting: `rendererFactory`** + +Globally, `Foo` has a specialized renderer registered: +```java +GlobalEasyGridConfiguration.forType(Foo.class) + .setRendererFactory(FooRenderers.of(...)); +``` +Independently, an instance configures Entity columns with a generic formatter: +```java +easyGrid.typeConfiguration(Entity.class).setFormatter(e -> e.getId().toString()); +``` + +Under scope-first, `I·Entity` wins over `G·Foo`. The Foo-specific renderer — which encodes *what a Foo looks like*, arguably a type invariant — gets silently replaced by a generic Entity formatter that the developer may not have intended to apply to Foo. + +Type-first preserves the type contract: `G·Foo` sits above `I·Entity`, so Foo's rendering semantics are honoured even across scope boundaries. + +--- + +## Liskov Substitution Principle + +LSP says: a `Foo` must be usable wherever an `Entity` is expected, without callers needing to know the difference. + +Applied to configuration, it has two implications that pull in opposite directions: + +**LSP supports scope-first for behavioural settings.** If you configure `Entity` at instance level — null representation, alignment, a locale-specific formatter — you are specifying how *this grid* handles all Entity values. A `Foo` substituting for `Entity` should satisfy those same observable postconditions. Refusing to inherit `I·Entity` because Foo has a `G·Foo` entry would mean the grid behaves differently for `Foo` vs `Entity` in ways the calling code did not anticipate. + +**LSP supports type-first for type-defining settings.** LSP also requires that subtypes honour their own invariants. If `Foo` has a type-level rendering contract — *this is how a Foo is displayed* — then substituting a generic `Entity` renderer for it violates the Foo invariant. The renderer is not just a postcondition on the grid; it is a property of `Foo` itself. + +The tension resolves in favour of **scope-first for all properties**. The type-first argument rests on the assumption that `I·Entity` displaces `G·Foo` unintentionally — but instance configuration is always deliberate. A developer who writes `forType(Entity.class).setFormatter(...)` knows `Foo IS-AN Entity`; if they wanted Foo to keep its global renderer they would have set `I·Foo` separately. The argument collapses entirely at broad overrides such as `forType(Object.class).setFormatter(...)`, where the developer has unambiguously stated that everything in this grid uses this formatter and global type-specific registrations cannot claim precedence. **Global registrations are defaults. Instance registrations are decisions. Decisions outrank defaults regardless of type specificity.** + +--- + +## Current Implementation + +The implementation uses **scope-first** for all properties. For a `Foo extends Entity` column the effective chain is: + +``` +per-column → I·Foo → I·Entity → I·Object → G·Foo → G·Entity → G·Object → Default +``` + +This is built in two pieces: + +**Instance chain** — `EasyGridConfigurationClassMap.getOrCreate(Foo)` walks the Java class hierarchy within the same map, producing a `ColumnConfigurationImpl` chain: + +``` +I·Foo(impl) → I·Entity(impl) → I·Object(impl) +``` + +**Global chain** — `GlobalEasyGridConfiguration.resolve(Foo)` similarly produces: + +``` +G·Foo(impl) → G·Entity(impl) → G·Object(impl) +``` + +**Bridge** — `InstanceEasyGridConfiguration.forType(Foo)` wraps both into a `ColumnConfigurationLink`: + +``` +ColumnConfigurationLink(primary = I·Foo chain, fallback = G·Foo chain) +``` + +`ColumnConfigurationLink.get()` consults the primary chain first and only falls back to the global chain when the primary returns `null` for a given property. + +**Per-column** — `InstanceEasyGridConfiguration.resolve(Foo)` wraps the link in a fresh `ColumnConfigurationImpl` whose fields are overridden by column-level setters (`setNullRepresentation`, `setFormatter`, `setRendererFactory`). + +**Default** — when the entire chain returns `null` for `getRendererFactory()`, `EasyColumn.createRenderer` applies a `ColumnConfigurationTextRenderer` with null-representation support as the last resort. diff --git a/FEATURE_ROW_ACTIONS.md b/FEATURE_ROW_ACTIONS.md new file mode 100644 index 0000000..69657ba --- /dev/null +++ b/FEATURE_ROW_ACTIONS.md @@ -0,0 +1,73 @@ +# Feature: Row Actions + +Row actions are buttons or menu items displayed in a dedicated actions column, created and managed by `EasyGrid` on the wrapped grid. + +## API + +### `EasyGrid` methods + +```java +// Add an action button (label + icon) +EasyRowAction addRowAction(String label, VaadinIcon icon, SerializableConsumer handler); + +// Add an action button with a theme variant +EasyRowAction addRowAction(String label, VaadinIcon icon, ButtonVariant variant, SerializableConsumer handler); + +// Render all actions as a context menu (overflow menu) instead of inline buttons +void setRowActionsAsMenu(boolean asMenu); + +// Access the underlying Grid.Column for header, width, freezing, etc. +Grid.Column getActionsColumn(); +``` + +### `EasyRowAction` + +```java +public class EasyRowAction { + // Conditional visibility + EasyRowAction withVisibleWhen(SerializablePredicate predicate); + + // Conditional enablement + EasyRowAction withEnabledWhen(SerializablePredicate predicate); + + // Tooltip + EasyRowAction withTooltip(String tooltip); + EasyRowAction withTooltip(SerializableFunction tooltipProvider); + + // Confirmation dialog before executing the action + EasyRowAction withConfirmation(String message); + EasyRowAction withConfirmation(String title, String message); +} +``` + +## Usage + +```java +// Inline action buttons +easyGrid.addRowAction("Edit", VaadinIcon.EDIT, person -> { + editPerson(person); +}); + +easyGrid.addRowAction("Delete", VaadinIcon.TRASH, ButtonVariant.LUMO_ERROR, person -> { + personService.delete(person); + easyGrid.getDataProvider().refreshAll(); +}).withConfirmation("Are you sure you want to delete this person?"); + +// Actions as a context menu (overflow menu) instead of inline buttons +easyGrid.setRowActionsAsMenu(true); + +// Conditional visibility +easyGrid.addRowAction("Activate", VaadinIcon.CHECK, person -> { + personService.activate(person); +}).withVisibleWhen(person -> !person.isActive()); + +easyGrid.addRowAction("Deactivate", VaadinIcon.CLOSE, person -> { + personService.deactivate(person); +}).withVisibleWhen(person -> person.isActive()); + +// Configure the actions column via the underlying Grid.Column +easyGrid.getActionsColumn() + .setHeader("Actions") + .setWidth("150px") + .setFrozenToEnd(true); +``` diff --git a/README.md b/README.md index 31da361..e608948 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Published on Vaadin Directory](https://img.shields.io/badge/Vaadin%20Directory-published-00b4f0.svg)](https://vaadin.com/directory/component/easy-grid-addon) [![Stars on vaadin.com/directory](https://img.shields.io/vaadin-directory/star/app-layout-addon.svg)](https://vaadin.com/directory/component/easy-grid-addon) -[![Build Status](https://jenkins.flowingcode.com/job/easy-grid-addon/badge/icon)](https://jenkins.flowingcode.com/job/easy-grid-addon) +[![Build Status](https://jenkins.flowingcode.com/job/EasyGrid-addon/badge/icon)](https://jenkins.flowingcode.com/job/EasyGrid-addon) [![Maven Central](https://img.shields.io/maven-central/v/com.flowingcode.vaadin.addons/easy-grid-addon)](https://mvnrepository.com/artifact/com.flowingcode.vaadin.addons/easy-grid-addon) # Easy Grid Add-on diff --git a/SPECIFICATIONS.md b/SPECIFICATIONS.md index 0994cd2..2d74afa 100644 --- a/SPECIFICATIONS.md +++ b/SPECIFICATIONS.md @@ -2,296 +2,253 @@ ## 1. Overview -The Easy Grid Add-on is a Vaadin Flow component that automatically generates a fully functional, sortable data grid from a Java POJO definition. It uses reflection to discover bean properties, maps them to appropriately typed and formatted columns, and provides a clean Java API for controlling column visibility, ordering, rendering, row actions, and data export via the Grid Exporter Add-on. +The Easy Grid Add-on provides `EasyGrid`, a Vaadin `Composite` component that wraps an internally created `Grid`. It uses reflection to discover bean properties, maps them to appropriately typed and formatted columns, and provides a clean Java API for controlling column ordering and type-specific rendering. + +`EasyGrid` is a component — it is added to the layout directly. Data binding and all other standard `Grid` features are accessed directly on the `EasyGrid` instance, which delegates them to the wrapped `Grid`. The wrapped `Grid` is accessible via `getWrappedGrid()` for any configuration not covered by the delegating API. + +For advanced cases where a custom `Grid` subclass must be supplied (e.g. `TreeGrid`), use `EasyGridWrapper` instead — it accepts a caller-provided `GRID extends Grid` and otherwise provides the same API. ## 2. Core Concepts ### 2.1 Automatic Column Discovery -Given a POJO class `T`, `EasyGrid` introspects its properties (via getter/setter conventions) and creates a `Grid.Column` for each one, with appropriate renderers and sorting behavior. +Given a POJO class `T`, `EasyGrid` introspects its properties (via getter/setter conventions) and creates a `Grid.Column` for each one on the wrapped grid, with appropriate renderers and sorting behavior. ### 2.2 Type-to-Renderer Mapping Each Java type maps to a default column renderer and configuration: -| Java Type | Renderer | Alignment | Sorting | -|-----------|----------|-----------|---------| -| `String` | `TextRenderer` | Start | Alphabetical | -| `Integer`, `int` | `NumberRenderer` | End | Numeric | -| `Long`, `long` | `NumberRenderer` | End | Numeric | -| `Double`, `double`, `Float`, `float` | `NumberRenderer` | End | Numeric | -| `BigDecimal` | `NumberRenderer` | End | Numeric | -| `Boolean`, `boolean` | `TextRenderer` ("Yes"/"No") | Center | — | -| `LocalDate` | `LocalDateRenderer` | Start | Chronological | -| `LocalDateTime` | `LocalDateTimeRenderer` | Start | Chronological | -| `LocalTime` | `TextRenderer` (formatted) | Start | Chronological | -| `Enum` | `TextRenderer` (name) | Start | Alphabetical | +| Java Type | Renderer | Default Format | Alignment | Sorting | +|-----------|----------|----------------|-----------|---------| +| `String` | `TextRenderer` | — | Start | Alphabetical | +| `Integer`, `int` | `NumberRenderer` | — | End | Numeric | +| `Long`, `long` | `NumberRenderer` | — | End | Numeric | +| `Double`, `double`, `Float`, `float` | `NumberRenderer` | — | End | Numeric | +| `BigDecimal` | `NumberRenderer` | — | End | Numeric | +| `Boolean`, `boolean` | `TextRenderer` | "true"/"false" | Center | — | +| `LocalDate` | `LocalDateRenderer` | `yyyy-MM-dd` | Start | Chronological | +| `LocalDateTime` | `LocalDateTimeRenderer` | `yyyy-MM-dd HH:mm:ss` | Start | Chronological | +| `LocalTime` | `TextRenderer` (formatted) | — | Start | Chronological | +| `Enum` | `TextRenderer` | `toString()` | Start | Alphabetical | Custom type mappings can be registered globally or per-grid instance. +**Boolean rendering:** `Boolean` columns render as the string literals `"true"` or `"false"` by default. Any other representation ("Yes"/"No", "On"/"Off", localised strings, etc.) requires a custom formatter — typically one that delegates to an application i18n provider. + +**Enum rendering:** Enum columns render using `Enum.toString()`, not `Enum.name()`. Per the Java documentation for `Enum.name()`: *"Most programmers should use the toString method in preference to this one, as the toString method may return a more user-friendly name."* Enums that need human-readable labels should override `toString()` accordingly. + ### 2.3 Sorting -All columns backed by `Comparable` property types are sortable by default. Multi-column sorting is supported. Sorting can be disabled per column. +All columns backed by `Comparable` property types are sortable by default. Multi-column sorting is supported. -### 2.4 Data Binding +For each sortable column, two distinct things are wired up automatically: -`EasyGrid` wraps Vaadin's `Grid` and supports all standard data provider mechanisms: +- **In-memory comparator** — set from the column's `ValueProvider` whenever the column's value type implements `Comparable` or is a primitive. Used by `ListDataProvider`-backed grids. +- **Backend sort property** — set to the bean property name on columns added via `addColumn(String)` (or any of the constructors that resolve named properties). Used by `BackEndDataProvider`-backed grids to map a column sort request to a backend sort key. Columns added via `addColumn(Class, ValueProvider)` have no backend sort property and must have one set explicitly with `EasyColumn.setSortProperty(...)` if backend sorting is needed. -- In-memory lists (`setItems(List)`) -- Lazy data providers (`setDataProvider(DataProvider)`) -- Callback data providers for backend integration +The backend sort property defaults to the same string used as the column key, but the two are independent after creation: calling `setSortProperty(...)` or `setKey(...)` later changes only the one called. ## 3. API Design ### 3.1 Construction +`EasyGrid` creates its own `Grid` internally. The caller adds the `EasyGrid` instance to the layout and calls `setItems()` on it. + ```java // Basic: all discovered properties become columns -EasyGrid grid = new EasyGrid<>(Person.class); -grid.setItems(personService.findAll()); +EasyGrid easyGrid = new EasyGrid<>(Person.class); +easyGrid.setItems(personService.findAll()); +add(easyGrid); // Selective: only specified properties become columns, in order -EasyGrid grid = new EasyGrid<>(Person.class, "firstName", "lastName", "email", "age"); -grid.setItems(personService.findAll()); +EasyGrid easyGrid = new EasyGrid<>(Person.class, "firstName", "lastName", "email", "age"); +easyGrid.setItems(personService.findAll()); +add(easyGrid); + +// Manual: suppress automatic column creation; add columns explicitly +EasyGrid easyGrid = new EasyGrid<>(Person.class, false); +easyGrid.addColumn("firstName"); +easyGrid.addColumn("lastName"); +easyGrid.addColumn(String.class, person -> person.getAddress().getCity()); +easyGrid.setItems(personService.findAll()); +add(easyGrid); ``` -### 3.2 Column Configuration — Fluent API +For cases where a custom `Grid` subclass must be supplied, use `EasyGridWrapper`: ```java -EasyGrid grid = new EasyGrid<>(Person.class); - -// Configure individual columns -grid.getColumnConfig("firstName") - .withHeader("First Name") - .withSortable(true) - .withWidth("200px") - .withResizable(true); - -grid.getColumnConfig("age") - .withHeader("Age") - .withTextAlign(ColumnTextAlign.END); +TreeGrid treeGrid = new TreeGrid<>(); +EasyGridWrapper> wrapper = + new EasyGridWrapper<>(treeGrid, Person.class, "firstName", "lastName"); +wrapper.setItems(personService.findAll()); +add(wrapper); +// access the wrapped grid for TreeGrid-specific configuration +wrapper.getWrappedGrid().setItemHierarchyData(...); +``` -grid.getColumnConfig("birthDate") - .withHeader("Date of Birth") - .withDateFormat("dd/MM/yyyy"); +`EasyGrid` also provides a typed overload for columns not backed by a named bean property: -grid.getColumnConfig("subscriber") - .withHeader("Subscribed") - .withBooleanLabels("Active", "Inactive"); +```java +// Adds a typed column that participates in the configuration tree by type, +// but has no auto-generated key or header +EasyColumn addColumn(Class type, ValueProvider getter); +``` -// Hide columns -grid.hideColumns("id", "createdAt", "updatedAt"); +### 3.2 Column Ordering and Visibility +```java // Set column order (only listed columns shown, in this order) -grid.setColumnOrder("firstName", "lastName", "email", "birthDate", "age"); +easyGrid.setColumnOrder("firstName", "lastName", "email", "birthDate", "age"); -// Freeze columns (always visible when scrolling horizontally) -grid.getColumnConfig("firstName").withFrozen(true); +// Hide columns +easyGrid.hideColumns("id", "createdAt", "updatedAt"); ``` -### 3.3 Column Configuration Wrapper — `EasyColumnConfig` +### 3.3 Column Configuration — `EasyColumn` -The `getColumnConfig(String propertyName)` method returns a fluent wrapper: +`getColumn(String propertyName)` returns an `EasyColumn` that provides both type-specific formatting and direct access to all standard `Grid.Column` setters for fluent configuration. The underlying `Grid.Column` is also accessible via `EasyColumn.getColumn()` for any configuration not covered by the delegating API. ```java -public class EasyColumnConfig { - // Header - EasyColumnConfig withHeader(String header); - EasyColumnConfig withHeader(Component headerComponent); - - // Footer - EasyColumnConfig withFooter(String footer); - EasyColumnConfig withFooter(Component footerComponent); - - // Sizing - EasyColumnConfig withWidth(String width); - EasyColumnConfig withFlexGrow(int flexGrow); - EasyColumnConfig withAutoWidth(boolean autoWidth); - EasyColumnConfig withResizable(boolean resizable); - - // Sorting - EasyColumnConfig withSortable(boolean sortable); - EasyColumnConfig withSortProperty(String... properties); - - // Alignment - EasyColumnConfig withTextAlign(ColumnTextAlign align); - - // Freezing - EasyColumnConfig withFrozen(boolean frozen); - EasyColumnConfig withFrozenToEnd(boolean frozen); - - // Visibility - EasyColumnConfig withVisible(boolean visible); - - // Custom rendering - EasyColumnConfig withRenderer(Renderer renderer); - EasyColumnConfig withComponentRenderer(SerializableFunction componentProvider); - EasyColumnConfig withValueFormatter(SerializableFunction formatter); - - // Type-specific formatting - EasyColumnConfig withDateFormat(String pattern); - EasyColumnConfig withDateTimeFormat(String pattern); - EasyColumnConfig withNumberFormat(String pattern); - EasyColumnConfig withBooleanLabels(String trueLabel, String falseLabel); - - // Access underlying Vaadin column +public class EasyColumn { + + // Type-specific formatting (EasyGrid-managed, applied to the column renderer) + EasyColumn setNullRepresentation(String nullRepresentation); + EasyColumn setFormatter(SerializableFunction formatter); + EasyColumn setRendererFactory(RendererFactory rendererFactory); + EasyColumn setTextAlign(ColumnTextAlign textAlign); + + // Cast-checked type narrowing — succeeds when the column's value type is a subtype of S + EasyColumn as(Class type); + + // The column value type + Class getType(); + + // Standard Grid.Column configuration — delegated for fluent chaining + EasyColumn setHeader(String headerText); + EasyColumn setHeader(Component headerComponent); + EasyColumn setFooter(String footerText); + EasyColumn setFooter(Component footerComponent); + EasyColumn setWidth(String width); + EasyColumn setFlexGrow(int flexGrow); + EasyColumn setAutoWidth(boolean autoWidth); + EasyColumn setResizable(boolean resizable); + EasyColumn setSortable(boolean sortable); + EasyColumn setSortProperty(String... properties); + EasyColumn setFrozen(boolean frozen); + EasyColumn setFrozenToEnd(boolean frozenToEnd); + EasyColumn setVisible(boolean visible); + EasyColumn setKey(String key); + // ... and other Grid.Column setters + + // Access the underlying Vaadin column for configuration not covered above Grid.Column getColumn(); } ``` -### 3.4 Row Actions - -Row actions are buttons or menu items displayed in an actions column, typically at the end of each row. +Usage example: ```java -// Add action buttons per row -grid.addRowAction("Edit", VaadinIcon.EDIT, person -> { - editPerson(person); -}); - -grid.addRowAction("Delete", VaadinIcon.TRASH, ButtonVariant.LUMO_ERROR, person -> { - personService.delete(person); - grid.getDataProvider().refreshAll(); -}); - -// Actions as a context menu (overflow menu) instead of inline buttons -grid.setRowActionsAsMenu(true); - -// Conditional action visibility -grid.addRowAction("Activate", VaadinIcon.CHECK, person -> { - personService.activate(person); -}).withVisibleWhen(person -> !person.isActive()); - -grid.addRowAction("Deactivate", VaadinIcon.CLOSE, person -> { - personService.deactivate(person); -}).withVisibleWhen(person -> person.isActive()); - -// Configure the actions column -grid.getActionsColumnConfig() - .withHeader("Actions") - .withWidth("150px") - .withFrozenToEnd(true); +// Format dates using a renderer factory from the renderer utility classes +easyGrid.getColumn("birthDate") + .as(LocalDate.class) + .setRendererFactory(LocalDateRenderers.of("dd/MM/yyyy")); + +// Format booleans with a custom formatter +easyGrid.getColumn("subscriber") + .as(Boolean.class) + .setFormatter(b -> b ? "Active" : "Inactive"); + +// Standard Grid.Column configuration available directly on EasyColumn +easyGrid.getColumn("firstName") + .setHeader("First Name") + .setFrozen(true) + .setWidth("200px"); ``` -### 3.5 Row Action Wrapper — `EasyRowAction` +### 3.4 Row Actions -```java -public class EasyRowAction { - // Conditional visibility - EasyRowAction withVisibleWhen(SerializablePredicate predicate); +See [FEATURE_ROW_ACTIONS.md](FEATURE_ROW_ACTIONS.md). - // Conditional enablement - EasyRowAction withEnabledWhen(SerializablePredicate predicate); +### 3.5 Type Configuration Tree - // Tooltip - EasyRowAction withTooltip(String tooltip); - EasyRowAction withTooltip(SerializableFunction tooltipProvider); +Column display configuration is resolved through a three-level tree, from most to least specific: - // Confirmation dialog before executing the action - EasyRowAction withConfirmation(String message); - EasyRowAction withConfirmation(String title, String message); -} -``` +| Level | API | Scope | +|---|---|---| +| **Column** | `EasyColumn` setters | One specific column | +| **Instance** | `EasyGrid.typeConfiguration(Class)` | All columns of that type in one grid | +| **Global** | `GlobalEasyGridConfiguration.forType(Class)` | All grids in the application | -### 3.6 Grid Exporter Integration +Within each level the class hierarchy is walked before the tree falls through to the next level (scope-first). The full resolution order for a `Foo extends Entity` column is: -The add-on integrates with the Grid Exporter Add-on to provide data export capabilities. +``` +Column·Foo + → Instance·Foo → Instance·Entity → Instance·Object + → Global·Foo → Global·Entity → Global·Object + → Built-in default +``` -```java -// Enable export with default formats (Excel, CSV, Docx) -grid.enableExport(); +The first non-`null` value found wins. See [CONFIGURATION_RESOLUTION.md](CONFIGURATION_RESOLUTION.md) for the rationale behind scope-first ordering. -// Enable specific export formats -grid.enableExport(ExportFormat.EXCEL, ExportFormat.CSV); +**Column level** — `EasyGrid.addColumn(…)` returns an `EasyColumn` whose setters write into an isolated configuration node at the top of the chain for that column only: -// Configure export -grid.getExportConfig() - .withFileName("person-report") - .withSheetName("People") - .withTitle("Person Report"); +```java +easyGrid.addColumn("active").setNullRepresentation("—"); +easyGrid.addColumn("salary").setTextAlign(ColumnTextAlign.END); +``` -// Custom column export configuration (e.g., different header for export) -grid.getColumnConfig("firstName").withExportHeader("Given Name"); +**Instance level** — `EasyGrid.typeConfiguration(Class)` returns the instance-level `ColumnConfiguration` for a type. Changes apply to every column of that type on this grid: -// Exclude a column from export (e.g., the actions column) -grid.getColumnConfig("actions").withExportable(false); +```java +easyGrid.typeConfiguration(BigDecimal.class) + .setRendererFactory(NumberRenderers.of("%,.2f", Locale.US)); ``` -### 3.7 Global Type Configuration - -Register custom column configurations that apply to all `EasyGrid` instances: +**Global level** — `GlobalEasyGridConfiguration.forType(Class)` returns the application-wide `ColumnConfiguration`. Call `GlobalEasyGridConfiguration.freeze()` after startup to prevent further modifications: ```java -// Register a global formatter for a custom type -EasyGrid.registerTypeConfig(Money.class, config -> { - config.withTextAlign(ColumnTextAlign.END); - config.withValueFormatter(money -> money.getCurrency() + " " + money.getAmount()); -}); - -// Register a global renderer for nested types -EasyGrid.registerTypeConfig(Address.class, config -> { - config.withValueFormatter(address -> - address.getStreet() + ", " + address.getCity() - ); -}); +GlobalEasyGridConfiguration.forType(LocalDate.class) + .setRendererFactory(LocalDateRenderers.of("dd/MM/yyyy")); +GlobalEasyGridConfiguration.freeze(); ``` -### 3.8 Selection +#### Null Representation + +The `nullRepresentation` property controls what is displayed when a column value is `null`. The built-in global default registers `""` (empty string) on `Object.class`, so every column starts with an empty cell for `null` values. Override it at any level: ```java -// Single selection (default) -grid.setSelectionMode(Grid.SelectionMode.SINGLE); -grid.addSelectionListener(event -> { - event.getFirstSelectedItem().ifPresent(this::showDetails); -}); - -// Multi-selection -grid.setSelectionMode(Grid.SelectionMode.MULTI); -grid.addSelectionListener(event -> { - Set selected = event.getAllSelectedItems(); - // bulk action -}); +// All columns in this grid show "–" for null +easyGrid.typeConfiguration(Object.class).setNullRepresentation("–"); + +// Only the "email" column shows "(none)" for null +easyGrid.addColumn("email").setNullRepresentation("(none)"); ``` -### 3.9 Filtering +#### Type Hierarchy Support -`EasyGrid` supports in-memory filtering for list-based data providers: +Inside each scope level, configuration walks the Java class hierarchy. When a configuration is requested for `Foo` and none exists, it is created with `Foo`'s superclass configuration as its parent, continuing up to `Object`. Primitive types are mapped to their boxed counterparts before hierarchy walking (`int` → `Integer`, `boolean` → `Boolean`, etc.). -```java -// Add a header filter row (auto-generated filter fields per column) -grid.enableHeaderFilters(); +A global `Number.class` renderer factory is therefore automatically inherited by `Integer`, `Long`, `BigDecimal`, and every other `Number` subtype unless a more specific configuration overrides it. -// Programmatic filtering -grid.setFilter(person -> - person.getAge() >= 18 && person.isActive() -); +#### Renderer Utility Classes -// Combined with external filter (for use with EasyCRUD) -grid.setExternalFilter(SerializablePredicate filter); -``` +Three `@UtilityClass` types in `com.flowingcode.vaadin.addons.easygrid.renderers` produce `RendererFactory` instances for common value types: -### 3.10 Events +- **`LocalDateRenderers`** — wraps `LocalDateRenderer`; overloads accept a format pattern, locale, null representation, or a `DateTimeFormatter` supplier. +- **`LocalDateTimeRenderers`** — wraps `LocalDateTimeRenderer`; same overloads as `LocalDateRenderers`. +- **`NumberRenderers`** — wraps `NumberRenderer`; overloads accept a `NumberFormat`, a `Locale`, or a `Formatter` pattern string with optional locale and null representation. ```java -// Row click -grid.addItemClickListener(event -> { - showDetails(event.getItem()); -}); - -// Row double-click -grid.addItemDoubleClickListener(event -> { - editPerson(event.getItem()); -}); - -// Sort change -grid.addSortListener(event -> { - // handle sort change -}); +GlobalEasyGridConfiguration.forType(LocalDate.class) + .setRendererFactory(LocalDateRenderers.of("dd/MM/yyyy", Locale.UK)); + +GlobalEasyGridConfiguration.forType(Number.class) + .setRendererFactory(NumberRenderers.of(NumberFormat.getInstance())); ``` ## 4. Default Header Generation -When no explicit header is provided, headers are auto-generated from property names using camelCase-to-title-case conversion: +When a column is added by `EasyGrid` and no explicit header has been set on it, the header is auto-generated from the property name using camelCase-to-title-case conversion: | Property Name | Generated Header | |--------------|-----------------| @@ -306,62 +263,53 @@ When no explicit header is provided, headers are auto-generated from property na Nested properties can be referenced using dot notation: ```java -grid.setColumnOrder("firstName", "lastName", "address.city", "address.postalCode"); +EasyGrid easyGrid = new EasyGrid<>(Person.class, + "firstName", "lastName", "address.city", "address.postalCode"); -grid.getColumnConfig("address.city") - .withHeader("City") - .withSortable(true); +easyGrid.getColumn("address.city") + .setHeader("City") + .setSortable(true); ``` ## 6. Serialization -`EasyGrid` must be fully serializable for Vaadin session persistence. All internal state, column configurations, action handlers, and renderers must be serializable. +`EasyGrid` must be fully serializable for Vaadin session persistence. All internal state, column configurations, and renderers must be serializable. ## 7. Usage Example — Complete ```java -// Minimal usage -EasyGrid simpleGrid = new EasyGrid<>(Person.class); -simpleGrid.setItems(personService.findAll()); -add(simpleGrid); - -// Customized usage -EasyGrid grid = new EasyGrid<>(Person.class); - -// Configure columns -grid.setColumnOrder("firstName", "lastName", "email", "birthDate", "age", "subscriber"); -grid.hideColumns("id", "createdAt", "updatedAt"); -grid.getColumnConfig("firstName").withHeader("Name").withFrozen(true); -grid.getColumnConfig("birthDate").withHeader("Born").withDateFormat("dd/MM/yyyy"); -grid.getColumnConfig("subscriber").withBooleanLabels("Yes", "No"); -grid.getColumnConfig("age").withTextAlign(ColumnTextAlign.END); - -// Row actions -grid.addRowAction("Edit", VaadinIcon.EDIT, this::editPerson); -grid.addRowAction("Delete", VaadinIcon.TRASH, ButtonVariant.LUMO_ERROR, person -> { - personService.delete(person); - grid.getDataProvider().refreshAll(); -}).withConfirmation("Are you sure you want to delete this person?"); - -// Enable export -grid.enableExport(); -grid.getExportConfig().withFileName("people-export"); - -// Set data -grid.setItems(personService.findAll()); - -add(grid); +EasyGrid easyGrid = new EasyGrid<>(Person.class); + +// Column ordering and visibility +easyGrid.setColumnOrder("firstName", "lastName", "email", "birthDate", "age", "subscriber"); +easyGrid.hideColumns("id", "createdAt", "updatedAt"); + +// Type-specific formatting +easyGrid.getColumn("birthDate").as(LocalDate.class) + .setRendererFactory(LocalDateRenderers.of("dd/MM/yyyy")); +easyGrid.getColumn("subscriber").as(Boolean.class) + .setFormatter(b -> b ? "Yes" : "No"); + +// Standard Grid.Column configuration via EasyColumn directly +easyGrid.getColumn("firstName") + .setHeader("Name") + .setFrozen(true); +easyGrid.getColumn("age") + .setTextAlign(ColumnTextAlign.END); + +// Data binding and adding to layout +easyGrid.setItems(personService.findAll()); +add(easyGrid); ``` ## 8. Dependencies - Vaadin Flow (24.x) -- Grid Exporter Add-on (for export functionality) - Lombok (per Flowing Code convention for new add-ons) ## 9. Non-Goals (Out of Scope) +- Data binding, selection, events, filtering, sorting configuration — use `Grid` or GridHelper directly - Inline cell editing (use EasyForm for editing) - Server-side data persistence - Tree grid / hierarchical data -- Lazy loading (supported via Vaadin's DataProvider API but not auto-configured) diff --git a/pom.xml b/pom.xml index 32bbb5c..2a1656e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.flowingcode.vaadin.addons easy-grid-addon - 1.0.0-SNAPSHOT + 0.1.0-SNAPSHOT Easy Grid Add-on Easy Grid Add-on for Vaadin Flow https://www.flowingcode.com/en/open-source/ @@ -20,7 +20,7 @@ UTF-8 ${project.basedir}/drivers 11.0.20 - 5.0.0 + 5.3.1 @@ -28,7 +28,7 @@ https://www.flowingcode.com - 2026 + 2020 Apache 2 @@ -76,6 +76,12 @@ 1.18.38 provided + + jakarta.servlet + jakarta.servlet-api + 6.0.0 + provided + com.flowingcode.vaadin.addons.demo commons-demo @@ -111,6 +117,28 @@ 5.9.1 test + + org.mockito + mockito-core + 5.17.0 + test + + + com.vaadin + vaadin-testbench-unit + test + + + com.vaadin + vaadin + test + + + com.github.javafaker + javafaker + 1.0.2 + test + @@ -272,7 +300,7 @@ none true - https://javadoc.io/doc/com.vaadin/vaadin-platform-javadoc/${vaadin.version} + https://javadoc.flowingcode.com/artifact/com.vaadin/vaadin-platform-javadoc/${vaadin.version} @@ -458,6 +486,14 @@ + + + com.flowingcode.vaadin.addons.demo + commons-demo-processor + ${flowingcode.commons.demo.version} + provided + + @@ -572,7 +608,7 @@ 21 21 - 25.0.3 + 25.1.3 @@ -584,7 +620,7 @@ jakarta.servlet jakarta.servlet-api 6.1.0 - test + provided diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/BeanPropertyDefinition.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/BeanPropertyDefinition.java new file mode 100644 index 0000000..61afaca --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/BeanPropertyDefinition.java @@ -0,0 +1,148 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid; + +import com.vaadin.flow.data.binder.PropertyDefinition; +import com.vaadin.flow.data.binder.PropertySet; +import com.vaadin.flow.data.binder.Setter; +import com.vaadin.flow.function.SerializableSupplier; +import com.vaadin.flow.function.ValueProvider; +import com.vaadin.flow.internal.BeanUtil; +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.function.Supplier; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +/** + * A {@code PropertyDefinition} that additionally exposes the introspected + * {@link PropertyDescriptor} for the underlying bean property. + * + * @param the bean type + * @param the property value type + */ +@SuppressWarnings("serial") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class BeanPropertyDefinition implements PropertyDefinition { + + private final PropertyDefinition pd; + private final SerializableSupplier descriptor; + + /** + * Returns a {@code BeanPropertyDefinition} for the given property, combining the + * {@code PropertySet} entry with the {@link PropertyDescriptor} obtained via introspection. + * + * @param the bean type + * @param propertySet the property set to look up the property in + * @param beanType the bean class, used for introspection + * @param propertyName the name of the property + * @return the resolved {@code BeanPropertyDefinition} + * @throws IllegalArgumentException if the property cannot be resolved + */ + public static BeanPropertyDefinition of(PropertySet propertySet, + Class beanType, String propertyName) { + PropertyDefinition property; + try { + property = propertySet.getProperty(propertyName).get(); + } catch (NoSuchElementException | IllegalArgumentException e) { + throw new IllegalArgumentException( + "Can't resolve property name '" + propertyName + "' from '" + propertySet + "'", e); + } + return new BeanPropertyDefinition<>(property, + () -> getPropertyDescriptor(beanType, propertyName)); + } + + private static PropertyDescriptor getPropertyDescriptor(Class beanType, + String propertyName) { + try { + return BeanUtil.getPropertyDescriptor(beanType, propertyName); + } catch (IntrospectionException e) { + throw new RuntimeReflectiveOperationException(e); + } + } + + /** + * Returns the {@code PropertyDescriptor} for the underlying bean property, or {@code null} if + * no descriptor supplier was provided. + * + * @return the {@code PropertyDescriptor}, or {@code null} if no descriptor supplier was provided + */ + public PropertyDescriptor getDescriptor() { + return Optional.ofNullable(descriptor).map(Supplier::get).orElse(null); + } + + @Override + public ValueProvider getGetter() { + return pd.getGetter(); + } + + @Override + public Optional> getSetter() { + return pd.getSetter(); + } + + @Override + public Class getType() { + return pd.getType(); + } + + @Override + public Class getPropertyHolderType() { + return pd.getPropertyHolderType(); + } + + @Override + public String getName() { + return pd.getName(); + } + + @Override + public String getTopLevelName() { + return pd.getTopLevelName(); + } + + @Override + public String getCaption() { + return pd.getCaption(); + } + + @Override + public PropertySet getPropertySet() { + return pd.getPropertySet(); + } + + @Override + public PropertyDefinition getParent() { + return pd.getParent(); + } + + @Override + public boolean isSubProperty() { + return pd.isSubProperty(); + } + + @Override + public boolean isGenericType() { + return pd.isGenericType(); + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/EasyColumn.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/EasyColumn.java new file mode 100644 index 0000000..53a21f6 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/EasyColumn.java @@ -0,0 +1,157 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid; + +import com.flowingcode.vaadin.addons.easygrid.config.ColumnConfiguration; +import com.flowingcode.vaadin.addons.easygrid.config.ColumnConfigurationTextRenderer; +import com.flowingcode.vaadin.addons.easygrid.renderers.RendererFactory; +import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.component.grid.ColumnTextAlign; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.data.renderer.Renderer; +import com.vaadin.flow.function.SerializableFunction; +import com.vaadin.flow.function.ValueProvider; +import java.io.Serializable; +import java.util.Optional; +import lombok.Getter; + +/** + * Wraps a {@link Grid.Column} and provides type-specific formatting for an EasyGridAddon-managed + * column. + * + * @param the grid bean type + * @param the column value type + */ +@SuppressWarnings("serial") +public final class EasyColumn implements IEasyGridColumn, Serializable { + + private final ColumnConfiguration config; + + @Getter + private final Grid.Column column; + + private final ValueProvider getter; + + @Getter + private final Class type; + + EasyColumn(ColumnConfiguration config, Grid.Column column, ValueProvider getter, + Class type) { + this.config = config; + this.column = column; + this.getter = getter; + this.type = type; + ComponentUtil.setData(column, EasyColumn.class, this); + Optional.ofNullable(config.getTextAlign()).ifPresent(column::setTextAlign); + } + + @SuppressWarnings("unchecked") + static EasyColumn getInstance(Grid.Column column) { + return ComponentUtil.getData(column, EasyColumn.class); + } + + /** + * Returns this column cast to value type {@code S}. + * Succeeds when the column's actual value type is a subtype of {@code S}; throws otherwise. + * + * @param the target value type + * @param type the target value type class + * @return this column cast to {@code EasyColumn} + * @throws ClassCastException if the column's actual value type is not a subtype of {@code type} + */ + public EasyColumn as(Class type) { + if (!type.isAssignableFrom(this.type)) { + throw new ClassCastException(String.format( + "column \"%s\" has value type %s which cannot be cast to %s", + column.getKey(), this.type.getName(), type.getName())); + } + @SuppressWarnings("unchecked") + EasyColumn result = (EasyColumn) this; + return result; + } + + static Renderer createRenderer(ColumnConfiguration config, + ValueProvider getter) { + var factory = config.getRendererFactory(); + if (factory != null) { + return factory.apply(getter); + } + return new ColumnConfigurationTextRenderer(getter, config); + } + + private void updateRenderer() { + column.setRenderer(createRenderer(config, getter)); + } + + /** + * Sets the string to display when the column value is {@code null}. Overrides any null + * representation inherited from the type or global configuration. Updates the underlying + * configuration and re-applies the renderer. + * + * @param nullRepresentation the string to show for {@code null} values + * @return this column, for fluent chaining + */ + public EasyColumn setNullRepresentation(String nullRepresentation) { + config.setNullRepresentation(nullRepresentation); + updateRenderer(); + return this; + } + + /** + * Sets a custom formatter that converts the column value to a display string. Overrides any + * type-specific or formatter-based renderer inherited from the configuration chain. + * + * @param formatter a function mapping a column value to its display string + * @return this column, for fluent chaining + */ + public EasyColumn setFormatter(SerializableFunction formatter) { + config.setFormatter(formatter); + updateRenderer(); + return this; + } + + /** + * Sets a custom renderer factory for this column. Overrides any type-specific or + * formatter-based renderer inherited from the configuration chain. + * + * @param rendererFactory a factory that creates a renderer from a value provider + * @return this column, for fluent chaining + */ + public EasyColumn setRendererFactory(RendererFactory rendererFactory) { + config.setRendererFactory(rendererFactory); + updateRenderer(); + return this; + } + + /** + * Sets the text alignment for this column's cells. Updates both the underlying + * {@link Grid.Column} and the {@link ColumnConfiguration}. + * + * @param textAlign the text alignment to apply + * @return this column, for fluent chaining + */ + @Override + public EasyColumn setTextAlign(ColumnTextAlign textAlign) { + IEasyGridColumn.super.setTextAlign(textAlign); + config.setTextAlign(textAlign); + return this; + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/EasyGrid.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/EasyGrid.java new file mode 100644 index 0000000..30df8ff --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/EasyGrid.java @@ -0,0 +1,69 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +package com.flowingcode.vaadin.addons.easygrid; + +import com.vaadin.flow.component.grid.Grid; +import lombok.NonNull; + +/** + * Concrete {@code EasyGridWrapper} that always wraps a plain {@link Grid}, creating it + * internally. This is the standard entry point for using the Easy Grid Add-on. + * + * @param the grid bean type + */ +@SuppressWarnings("serial") +public class EasyGrid extends EasyGridWrapper> { + + /** + * Creates an {@code EasyGrid} that discovers all top-level bean properties and adds a column for + * each. + * + * @param beanType the bean type to use, not {@code null} + * @throws NullPointerException if {@code beanType} is {@code null} + */ + public EasyGrid(@NonNull Class beanType) { + super(new Grid(), beanType); + } + + /** + * Creates an {@code EasyGrid} for the given bean type, optionally auto-creating columns. + * + * @param beanType the bean type to use, not {@code null} + * @param autoCreateColumns when {@code true}, columns are created automatically for the + * properties of the bean type + * @throws NullPointerException if {@code beanType} is {@code null} + */ + public EasyGrid(@NonNull Class beanType, boolean autoCreateColumns) { + super(new Grid(), beanType, autoCreateColumns); + } + + /** + * Creates an {@code EasyGrid} that adds columns for the specified properties in order. + * + * @param beanType the bean type to use, not {@code null} + * @param propertyNames the names of the properties for which columns are created + * @throws NullPointerException if {@code beanType} or {@code propertyNames} is {@code null} + */ + public EasyGrid(@NonNull Class beanType, String... propertyNames) { + super(new Grid(), beanType, propertyNames); + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/EasyGridComposite.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/EasyGridComposite.java new file mode 100644 index 0000000..db2db49 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/EasyGridComposite.java @@ -0,0 +1,91 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid; + +import com.vaadin.flow.component.Composite; +import com.vaadin.flow.component.HasSize; +import com.vaadin.flow.component.HasStyle; +import com.vaadin.flow.component.HasTheme; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.dataview.GridDataView; +import com.vaadin.flow.component.grid.dataview.GridLazyDataView; +import com.vaadin.flow.component.grid.dataview.GridListDataView; +import com.vaadin.flow.data.provider.HasDataGenerators; +import com.vaadin.flow.data.provider.HasDataView; +import com.vaadin.flow.data.provider.HasLazyDataView; +import com.vaadin.flow.data.provider.HasListDataView; +import java.util.function.Consumer; +import lombok.NonNull; +import lombok.experimental.Delegate; + +/** + * Base {@code Composite} wrapper around a {@link Grid} that delegates the subset of grid methods + * defined by {@link IEasyGridComposite} to the wrapped grid instance. Subclasses access the + * wrapped grid via {@link #getWrappedGrid()} and may add higher-level behaviour on top of this + * delegation layer. + * + * @param the grid bean type + * @param the concrete {@code Grid} subtype being wrapped + */ +@SuppressWarnings("serial") +class EasyGridComposite> extends Composite + implements HasStyle, HasSize, HasTheme, + HasDataGenerators, HasListDataView>, + HasDataView>, + HasLazyDataView> { + + non-sealed abstract class IEasyGridDelegate implements IEasyGridComposite { + } + + @Delegate(types = IEasyGridDelegate.class) + private final GRID grid; + + /** + * Creates a wrapper around the given grid. + * + * @param grid the grid to wrap, not {@code null} + */ + public EasyGridComposite(@NonNull GRID grid) { + this.grid = grid; + } + + @Override + protected GRID initContent() { + return grid; + } + + /** + * Returns the wrapped grid. + * + * @return the wrapped grid, never {@code null} + */ + public final GRID getWrappedGrid() { + return grid; + } + + /** + * Passes the wrapped grid to the given customizer, allowing direct configuration of the grid. + * + * @param customizer a consumer that configures the wrapped grid + */ + public void configure(Consumer customizer) { + customizer.accept(grid); + } +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/EasyGridWrapper.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/EasyGridWrapper.java new file mode 100644 index 0000000..164887b --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/EasyGridWrapper.java @@ -0,0 +1,273 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +package com.flowingcode.vaadin.addons.easygrid; + +import com.flowingcode.vaadin.addons.easygrid.config.ColumnConfiguration; +import com.flowingcode.vaadin.addons.easygrid.config.InstanceEasyGridConfiguration; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.data.binder.BeanPropertySet; +import com.vaadin.flow.data.binder.PropertyDefinition; +import com.vaadin.flow.data.binder.PropertySet; +import com.vaadin.flow.function.ValueProvider; +import com.vaadin.flow.shared.util.SharedUtil; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; +import lombok.Getter; +import lombok.NonNull; + +/** + * Wraps a {@link Grid} to provide bean property-based column creation and type-aware + * {@link ColumnConfiguration} resolution. Columns can be created automatically from all top-level + * bean properties, from an explicit list of property names, or individually via + * {@link #addColumn(String)} and {@link #addColumn(Class, ValueProvider)}. + * + *

{@link EasyGrid} is the standard concrete subclass for use with a plain {@code Grid}. + * + * @param the grid bean type + * @param the concrete {@code Grid} subtype being wrapped + */ +@SuppressWarnings("serial") +public class EasyGridWrapper> extends EasyGridComposite { + + @Getter + private final Class beanType; + private transient PropertySet propertySet; + + private final InstanceEasyGridConfiguration configuration = new InstanceEasyGridConfiguration(); + + /** + * Creates an {@code EasyGridWrapper} that discovers all top-level bean properties and adds a + * column for each. + * + * @param grid the grid to configure, not {@code null} + * @param beanType the bean type to use, not {@code null} + * @throws NullPointerException if either {@code grid} or {@code beanType} is {@code null} + */ + public EasyGridWrapper(@NonNull GRID grid, @NonNull Class beanType) { + this(grid, beanType, true); + } + + /** + * Creates an {@code EasyGridWrapper} for the given bean type, optionally auto-creating columns. + * + * @param grid the grid to configure, not {@code null} + * @param beanType the bean type to use, not {@code null} + * @param autoCreateColumns when {@code true}, columns are created automatically for the + * properties of the bean type + * @throws NullPointerException if either {@code grid} or {@code beanType} is {@code null} + */ + public EasyGridWrapper(@NonNull GRID grid, @NonNull Class beanType, + boolean autoCreateColumns) { + super(grid); + this.beanType = beanType; + if (autoCreateColumns) { + getPropertySet().getProperties().filter(p -> !p.isSubProperty()) + .map(PropertyDefinition::getName).forEach(this::addColumn); + } + } + + /** + * Creates an {@code EasyGridWrapper} that adds columns for the specified properties in order. + * + * @param grid the grid to configure, not {@code null} + * @param beanType the bean type to use, not {@code null} + * @param propertyNames the names of the properties for which columns are created + * @throws NullPointerException if either {@code grid} or {@code beanType} is {@code null}, or if + * {@code propertyNames} is {@code null} + */ + public EasyGridWrapper(@NonNull GRID grid, @NonNull Class beanType, String... propertyNames) { + super(grid); + this.beanType = beanType; + addColumns(propertyNames); + } + + private PropertySet getPropertySet() { + if (propertySet == null) { + propertySet = BeanPropertySet.get(beanType); + } + return propertySet; + } + + /** + * Returns the {@code EasyColumn} for the given property name, or {@code null} if no column with + * that key exists or the column was not created through the EasyGrid API. + * + * @param propertyName the property name used as the column key + * @return the {@code EasyColumn} for the given key, or {@code null} + */ + public EasyColumn getColumn(String propertyName) { + Grid.Column column = getWrappedGrid().getColumnByKey(propertyName); + return column != null ? EasyColumn.getInstance(column) : null; + } + + /** + * Adds a column for the given bean property. The column header is derived from the property name, + * and the column is made sortable if the property type implements {@link Comparable} or is a + * primitive type. + * + * @param propertyName the name of the bean property + * @return the {@code EasyColumn} for the added column + * @throws IllegalArgumentException if the property cannot be resolved or a column for it already + * exists + */ + public EasyColumn addColumn(String propertyName) { + return createEasyColumn(resolveProperty(propertyName)); + } + + /** + * Sets the order of the columns shown in the grid. Only the listed columns are visible after this + * call; all other columns are hidden. The columns are shown in the order they are listed. + * + * @param propertyNames the property names of the columns to show, in the desired order + * @throws NullPointerException if {@code propertyNames} is {@code null} + */ + public void setColumnOrder(@NonNull String... propertyNames) { + Set visible = new LinkedHashSet<>(Arrays.asList(propertyNames)); + List> ordered = new ArrayList<>(); + for (String name : visible) { + Grid.Column column = getWrappedGrid().getColumnByKey(name); + if (column != null) { + column.setVisible(true); + ordered.add(column); + } + } + getWrappedGrid().getColumns().stream() + .filter(c -> !visible.contains(c.getKey())) + .peek(c -> c.setVisible(false)) + .forEach(ordered::add); + getWrappedGrid().setColumnOrder(ordered); + } + + /** + * Hides the columns with the given property names. + * + * @param propertyNames the property names of the columns to hide + * @throws NullPointerException if {@code propertyNames} is {@code null} + */ + public void hideColumns(@NonNull String... propertyNames) { + Stream.of(propertyNames).map(getWrappedGrid()::getColumnByKey).filter(Objects::nonNull) + .forEach(c -> c.setVisible(false)); + } + + /** + * Adds columns for the given bean properties in order. + * + * @param propertyNames the names of the bean properties + * @throws NullPointerException if {@code propertyNames} is {@code null} + * @throws IllegalArgumentException if any property cannot be resolved or already has a column + */ + public void addColumns(@NonNull String... propertyNames) { + Stream.of(propertyNames).forEach(this::addColumn); + } + + /** + * Adds a column for the given type and value provider. The column is made sortable if the value + * type implements {@link Comparable} or is a primitive type. No column key or header is set + * automatically; use the returned {@code EasyColumn} to configure them. + * + * @param the column value type + * @param type the {@code Class} of the column value, used to resolve the {@link ColumnConfiguration} + * @param getter a value provider that extracts the column value from a bean instance; may return + * any subtype of {@code V} + * @return the {@code EasyColumn} for the added column + */ + public EasyColumn addColumn(Class type, ValueProvider getter) { + @SuppressWarnings("unchecked") + ValueProvider safeGetter = (ValueProvider) getter; + return createEasyColumn(type, safeGetter); + } + + @SuppressWarnings("unchecked") + private BeanPropertyDefinition resolveProperty(String propertyName) { + return (BeanPropertyDefinition) BeanPropertyDefinition.of(getPropertySet(), beanType, propertyName); + } + + /** + * Creates and adds a new column to the wrapped grid for the given bean property, applying the + * {@link ColumnConfiguration} resolved for the property's value type. + * + *

+ * If the property has a non-null name, it is used as the column key and as the source for a + * human-friendly header caption; an exception is thrown if the wrapped grid already has a column + * with that key. The column is made sortable when the value type implements {@link Comparable} or + * is a non-{@code void} primitive. + * + *

+ * Subclasses may override this method to customize how columns are created from bean properties + * (for example, to apply additional renderers or post-process the resulting {@code EasyColumn}). + * + * @param the column value type + * @param pd the bean property definition describing the column source + * @return the {@code EasyColumn} for the added column + * @throws IllegalArgumentException if the wrapped grid already has a column whose key matches the + * property name + */ + protected EasyColumn createEasyColumn(BeanPropertyDefinition pd) { + String propertyName = pd.getName(); + if (propertyName != null && getWrappedGrid().getColumnByKey(propertyName) != null) { + throw new IllegalArgumentException("Multiple columns for the same property: " + propertyName); + } + + var column = createEasyColumn(pd.getType(), pd.getGetter()); + + if (propertyName != null) { + String caption = SharedUtil.propertyIdToHumanFriendly(propertyName); + column.setHeader(caption); + column.setKey(propertyName); + if (column.getColumn().isSortable()) { + column.setSortProperty(propertyName); + } + } + + return column; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private EasyColumn createEasyColumn(Class type, ValueProvider getter) { + ColumnConfiguration config = configuration.resolve(type); + Grid.Column column = getWrappedGrid().addColumn(EasyColumn.createRenderer(config, getter)); + if (Comparable.class.isAssignableFrom(type) || type.isPrimitive()) { + column.setComparator((ValueProvider) getter); + } + return new EasyColumn<>(config, column, getter, type); + } + + + /** + * Returns the {@code ColumnConfiguration} for the given value type at the instance level, + * creating it if it does not yet exist. Modifications to the returned configuration apply to all + * columns of that type managed by this instance and take precedence over + * {@link com.flowingcode.vaadin.addons.easygrid.config.GlobalEasyGridConfiguration}. + * + * @param the column value type + * @param type the {@code Class} of the column value type + * @return the {@code ColumnConfiguration} for the given type + */ + public ColumnConfiguration typeConfiguration(Class type) { + return configuration.forType(type); + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/IEasyGridColumn.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/IEasyGridColumn.java new file mode 100644 index 0000000..fd429fb --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/IEasyGridColumn.java @@ -0,0 +1,330 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid; + +/* + * Portions of this interface are derived from the Vaadin Flow Grid API, + * Copyright 2000-2025 Vaadin Ltd., licensed under the Apache License, Version 2.0. + * See https://github.com/vaadin/flow-components for the original source. + */ + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.grid.ColumnTextAlign; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.SortOrderProvider; +import com.vaadin.flow.function.SerializableFunction; +import com.vaadin.flow.function.ValueProvider; +import java.util.Comparator; + +/** + * Defines the chainable column API exposed by {@link EasyColumn}, providing fluent delegating + * setters for the underlying {@link Grid.Column}. + * + * @param the grid bean type + * @param the column value type + */ +sealed interface IEasyGridColumn permits EasyColumn { + + /** + * Returns the underlying {@code Grid.Column}. + * + * @return the underlying {@code Grid.Column} + */ + Grid.Column getColumn(); + + /** + * Sets the text alignment for this column's cells. + * + * @param textAlign the text alignment to apply + * @return this column, for fluent chaining + */ + default EasyColumn setTextAlign(ColumnTextAlign textAlign) { + getColumn().setTextAlign(textAlign); + return (EasyColumn) this; + } + + /** + * Sets whether this column's width is automatically determined from its content. + * + * @param autoWidth {@code true} to enable auto-width, {@code false} to use a fixed width + * @return this column, for fluent chaining + */ + default EasyColumn setAutoWidth(boolean autoWidth) { + getColumn().setAutoWidth(autoWidth); + return (EasyColumn) this; + } + + /** + * Sets a comparator to use for in-memory sorting for this column. + * + * @param comparator the comparator to use + * @return this column, for fluent chaining + */ + default EasyColumn setComparator(Comparator comparator) { + getColumn().setComparator(comparator); + return (EasyColumn) this; + } + + /** + * Sets a comparator to use for in-memory sorting for this column based on a value provider. + * + * @param the comparable value type + * @param valueProvider a value provider that extracts the comparable sort key from a row item + * @return this column, for fluent chaining + */ + default > EasyColumn setComparator( + ValueProvider valueProvider) { + getColumn().setComparator(valueProvider); + return (EasyColumn) this; + } + + /** + * Sets the editor component for this column. + * + * @param component the editor component + * @return this column, for fluent chaining + */ + default EasyColumn setEditorComponent(Component component) { + getColumn().setEditorComponent(component); + return (EasyColumn) this; + } + + /** + * Sets a generator that provides the editor component for each row in this column. + * + * @param componentFunction a function that returns the editor component for a given row item + * @return this column, for fluent chaining + */ + default EasyColumn setEditorComponent( + SerializableFunction componentFunction) { + getColumn().setEditorComponent(componentFunction); + return (EasyColumn) this; + } + + /** + * Sets the flex grow ratio for this column. + * + * @param flexGrow the flex grow ratio + * @return this column, for fluent chaining + */ + default EasyColumn setFlexGrow(int flexGrow) { + getColumn().setFlexGrow(flexGrow); + return (EasyColumn) this; + } + + /** + * Sets a text footer for this column. + * + * @param footerText the footer text + * @return this column, for fluent chaining + */ + default EasyColumn setFooter(String footerText) { + getColumn().setFooter(footerText); + return (EasyColumn) this; + } + + /** + * Sets a component footer for this column. + * + * @param footerComponent the footer component + * @return this column, for fluent chaining + */ + default EasyColumn setFooter(Component footerComponent) { + getColumn().setFooter(footerComponent); + return (EasyColumn) this; + } + + /** + * Sets the part name for the footer cell of this column. + * + * @param footerPartName the part name for the footer cell + * @return this column, for fluent chaining + */ + default EasyColumn setFooterPartName(String footerPartName) { + getColumn().setFooterPartName(footerPartName); + return (EasyColumn) this; + } + + /** + * Sets whether this column is frozen, locking it to the start of the grid. + * + * @param frozen {@code true} to freeze this column, {@code false} to unfreeze it + * @return this column, for fluent chaining + */ + default EasyColumn setFrozen(boolean frozen) { + getColumn().setFrozen(frozen); + return (EasyColumn) this; + } + + /** + * Sets whether this column is frozen to the end of the grid. + * + * @param frozenToEnd {@code true} to freeze this column to the end, {@code false} to unfreeze it + * @return this column, for fluent chaining + */ + default EasyColumn setFrozenToEnd(boolean frozenToEnd) { + getColumn().setFrozenToEnd(frozenToEnd); + return (EasyColumn) this; + } + + /** + * Sets a text header for this column. + * + * @param headerText the header text + * @return this column, for fluent chaining + */ + default EasyColumn setHeader(String headerText) { + getColumn().setHeader(headerText); + return (EasyColumn) this; + } + + /** + * Sets a component header for this column. + * + * @param headerComponent the header component + * @return this column, for fluent chaining + */ + default EasyColumn setHeader(Component headerComponent) { + getColumn().setHeader(headerComponent); + return (EasyColumn) this; + } + + /** + * Sets the part name for the header cell of this column. + * + * @param headerPartName the part name for the header cell + * @return this column, for fluent chaining + */ + default EasyColumn setHeaderPartName(String headerPartName) { + getColumn().setHeaderPartName(headerPartName); + return (EasyColumn) this; + } + + /** + * Sets the key for this column, used to retrieve it via {@link Grid#getColumnByKey(String)}. + * + * @param key the column key + * @return this column, for fluent chaining + */ + default EasyColumn setKey(String key) { + getColumn().setKey(key); + return (EasyColumn) this; + } + + /** + * Sets a generator that returns a part name for a given row item in this column. + * + * @param partNameGenerator a function that returns the part name for a given row item + * @return this column, for fluent chaining + */ + default EasyColumn setPartNameGenerator(SerializableFunction partNameGenerator) { + getColumn().setPartNameGenerator(partNameGenerator); + return (EasyColumn) this; + } + + /** + * Sets whether this column is user-resizable. + * + * @param resizable {@code true} to make this column resizable, {@code false} to prevent resizing + * @return this column, for fluent chaining + */ + default EasyColumn setResizable(boolean resizable) { + getColumn().setResizable(resizable); + return (EasyColumn) this; + } + + /** + * Sets whether this column represents a row header for accessibility purposes. + * + * @param rowHeader {@code true} to mark this column as a row header, {@code false} otherwise + * @return this column, for fluent chaining + */ + default EasyColumn setRowHeader(boolean rowHeader) { + getColumn().setRowHeader(rowHeader); + return (EasyColumn) this; + } + + /** + * Sets whether this column is user-sortable. + * + * @param sortable {@code true} to make this column sortable, {@code false} to disable sorting + * @return this column, for fluent chaining + */ + default EasyColumn setSortable(boolean sortable) { + getColumn().setSortable(sortable); + return (EasyColumn) this; + } + + /** + * Sets the sort order provider for this column. + * + * @param provider the sort order provider + * @return this column, for fluent chaining + */ + default EasyColumn setSortOrderProvider(SortOrderProvider provider) { + getColumn().setSortOrderProvider(provider); + return (EasyColumn) this; + } + + /** + * Sets the backend properties used for sorting when this column is sorted. + * + * @param properties the backend property names used for sorting + * @return this column, for fluent chaining + */ + default EasyColumn setSortProperty(String... properties) { + getColumn().setSortProperty(properties); + return (EasyColumn) this; + } + + /** + * Sets a generator that produces a tooltip for a given row item in this column. + * + * @param tooltipGenerator a function that returns the tooltip text for a given row item + * @return this column, for fluent chaining + */ + default EasyColumn setTooltipGenerator(SerializableFunction tooltipGenerator) { + getColumn().setTooltipGenerator(tooltipGenerator); + return (EasyColumn) this; + } + + /** + * Sets whether this column is visible. + * + * @param visible {@code true} to show this column, {@code false} to hide it + * @return this column, for fluent chaining + */ + default EasyColumn setVisible(boolean visible) { + getColumn().setVisible(visible); + return (EasyColumn) this; + } + + /** + * Sets the width of this column as a CSS value. + * + * @param width the column width as a CSS value + * @return this column, for fluent chaining + */ + default EasyColumn setWidth(String width) { + getColumn().setWidth(width); + return (EasyColumn) this; + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/IEasyGridComposite.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/IEasyGridComposite.java new file mode 100644 index 0000000..f3810c1 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/IEasyGridComposite.java @@ -0,0 +1,240 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid; + +/* + * Portions of this interface are derived from the Vaadin Flow Grid API, + * Copyright 2000-2025 Vaadin Ltd., licensed under the Apache License, Version 2.0. + * See https://github.com/vaadin/flow-components for the original source. + */ + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.Grid.SelectionMode; +import com.vaadin.flow.component.grid.GridSelectionModel; +import com.vaadin.flow.component.grid.ItemClickEvent; +import com.vaadin.flow.component.grid.dataview.GridDataView; +import com.vaadin.flow.component.grid.dataview.GridLazyDataView; +import com.vaadin.flow.component.grid.dataview.GridListDataView; +import com.vaadin.flow.data.provider.BackEndDataProvider; +import com.vaadin.flow.data.provider.DataGenerator; +import com.vaadin.flow.data.provider.DataProvider; +import com.vaadin.flow.data.provider.InMemoryDataProvider; +import com.vaadin.flow.data.provider.ListDataProvider; +import com.vaadin.flow.data.selection.SelectionListener; +import com.vaadin.flow.function.ValueProvider; +import com.vaadin.flow.shared.Registration; +import java.util.Set; + +/** + * Defines the subset of {@link com.vaadin.flow.component.grid.Grid} methods that + * {@link EasyGridComposite} re-exposes by delegating to the wrapped grid instance. Used as the + * type argument for Lombok {@code @Delegate} on the {@link EasyGridComposite.IEasyGridDelegate} inner class. + * + * @param the grid bean type + */ +public sealed interface IEasyGridComposite permits EasyGridComposite.IEasyGridDelegate { + + /** + * Adds the given data generator. If the generator was already added, does nothing. + * + * @param generator the data generator to add + * @return a registration that can be used to remove the data generator + */ + Registration addDataGenerator(DataGenerator generator); + + /** + * Adds an item click listener to this component. + * + * @param listener the listener to add, not {@code null} + * @return a handle that can be used for removing the listener + */ + Registration addItemClickListener(ComponentEventListener> listener); + + /** + * Adds a selection listener to the current selection model. + *

+ * This is a shorthand for {@code grid.getSelectionModel().addSelectionListener()}. To get more + * detailed selection events, use {@link Grid#getSelectionModel()} and either + * {@link com.vaadin.flow.component.grid.GridSingleSelectionModel#addSingleSelectionListener(com.vaadin.flow.data.selection.SingleSelectionListener)} + * or + * {@link com.vaadin.flow.component.grid.GridMultiSelectionModel#addMultiSelectionListener(com.vaadin.flow.data.selection.MultiSelectionListener)} + * depending on the used selection mode. + * + * @param listener the listener to add + * @return a registration handle to remove the listener + * @throws UnsupportedOperationException if selection has been disabled with + * {@link SelectionMode#NONE} + */ + Registration addSelectionListener(SelectionListener, T> listener); + + /** + * Adds a new column that shows components. + *

+ * This is a shorthand for {@link Grid#addColumn(com.vaadin.flow.data.renderer.Renderer)} with a + * {@link com.vaadin.flow.data.renderer.ComponentRenderer}. + *

+ * NOTE: Using {@code ComponentRenderer} is not as efficient as the built in renderers or + * using {@link com.vaadin.flow.data.renderer.LitRenderer}. + *

+ * Every added column sends data to the client side regardless of its visibility state. Don't add + * a new column at all or use {@link Grid#removeColumn(Grid.Column)} to avoid sending extra data. + * + * @param componentProvider a value provider that will return a component for the given item + * @param the component type + * @return the new column + */ + Grid.Column addComponentColumn(ValueProvider componentProvider); + + /** + * Gets the generic data view for the grid. This data view should only be used when + * {@link #getListDataView()} or {@link #getLazyDataView()} is not applicable for the underlying + * data provider. + * + * @return the generic {@link com.vaadin.flow.data.provider.DataView} implementation for grid + * @see #getListDataView() + * @see #getLazyDataView() + */ + GridDataView getGenericDataView(); + + /** + * Gets the lazy data view for the grid. This data view should only be used when the items are + * provided lazily from the backend with: + *

    + *
  • {@link Grid#setItems(com.vaadin.flow.data.provider.CallbackDataProvider.FetchCallback)}
  • + *
  • {@link Grid#setItems(com.vaadin.flow.data.provider.CallbackDataProvider.FetchCallback, com.vaadin.flow.data.provider.CallbackDataProvider.CountCallback)}
  • + *
  • {@link #setItems(BackEndDataProvider)}
  • + *
+ * If the items are not fetched lazily an exception is thrown. When the items are in-memory, use + * {@link #getListDataView()} instead. + * + * @return the lazy data view that provides access to the data bound to the grid + */ + GridLazyDataView getLazyDataView(); + + /** + * Gets the list data view for the grid. This data view should only be used when the items are + * in-memory set with: + *
    + *
  • {@link Grid#setItems(java.util.Collection)}
  • + *
  • {@link Grid#setItems(Object[])}
  • + *
  • {@link #setItems(ListDataProvider)}
  • + *
+ * If the items are not in-memory an exception is thrown. When the items are fetched lazily, use + * {@link #getLazyDataView()} instead. + * + * @return the list data view that provides access to the items in the grid + */ + GridListDataView getListDataView(); + + /** + * This method is a shorthand that delegates to the currently set selection model. + * + * @see GridSelectionModel + * + * @return a set with the selected items, never {@code null} + */ + Set getSelectedItems(); + + /** + * Supply items with a {@code BackEndDataProvider} that lazy loads items from a backend. Note that + * component will query the data provider for the item count. In case that is not desired for + * performance reasons, use + * {@link Grid#setItems(com.vaadin.flow.data.provider.CallbackDataProvider.FetchCallback)} + * instead. + *

+ * The returned data view object can be used for further configuration, or later on fetched with + * {@link #getLazyDataView()}. For using in-memory data, like {@link java.util.Collection}, use + * {@link com.vaadin.flow.data.provider.HasListDataView#setItems(java.util.Collection)} instead. + * + * @param dataProvider {@code BackEndDataProvider} instance + * @return {@link com.vaadin.flow.data.provider.LazyDataView} instance for further configuration + */ + GridLazyDataView setItems(BackEndDataProvider dataProvider); + + /** + * Set a generic data provider for the component to use and returns the base {@link com.vaadin.flow.data.provider.DataView} + * that provides API to get information on the items. + *

+ * This method should be used only when the data provider type is not either + * {@link ListDataProvider} or {@link BackEndDataProvider}. + * + * @param dataProvider {@code DataProvider} instance to use, not {@code null} + * @return {@code DataView} providing information on the data + */ + GridDataView setItems(DataProvider dataProvider); + + /** + * Sets a {@code ListDataProvider} for the component to use and returns a {@link com.vaadin.flow.data.provider.ListDataView} + * that provides information and allows operations on the items. + * + * @param dataProvider {@code ListDataProvider} providing items to the component + * @return {@code ListDataView} providing access to the items + */ + GridListDataView setItems(ListDataProvider dataProvider); + + /** + * Sets an in-memory data provider for the component to use. + *

+ * Note! Using a {@link ListDataProvider} instead of an {@code InMemoryDataProvider} is + * recommended to get access to {@link com.vaadin.flow.data.provider.ListDataView} API by using + * {@link #setItems(ListDataProvider)}. + * + * @param dataProvider {@code InMemoryDataProvider} to use, not {@code null} + * @return {@link com.vaadin.flow.data.provider.DataView} providing information on the data + */ + GridDataView setItems(InMemoryDataProvider dataProvider); + + /** + * Sets the grid's selection mode. + *

+ * To use your custom selection model, you can use + * {@link Grid#setSelectionModel(GridSelectionModel, SelectionMode)}, see existing selection model + * implementations for example. + * + * @param selectionMode the selection mode to switch to, not {@code null} + * @return the used selection model + * + * @see SelectionMode + * @see GridSelectionModel + */ + GridSelectionModel setSelectionMode(SelectionMode selectionMode); + + /** + * Sets the defined columns as sortable, based on the given property names. + *

+ * This is a shortcut for setting all columns not sortable and then calling + * {@link Grid.Column#setSortable(boolean)} for each of the columns defined by the given + * propertyNames. + *

+ * You can set sortable columns for nested properties with dot notation, eg. + * {@code "property.nestedProperty"} + *

+ * Note: This method can only be used for a {@link Grid} created from a bean type + * with {@link Grid#Grid(Class)}. + * + * @param propertyNames the property names used to reference the columns + * + * @throws IllegalArgumentException if any of the propertyNames refers to a non-existing column + */ + void setSortableColumns(String... propertyNames); + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/RuntimeReflectiveOperationException.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/RuntimeReflectiveOperationException.java new file mode 100644 index 0000000..e1fabd2 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/RuntimeReflectiveOperationException.java @@ -0,0 +1,39 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid; + +/** + * Unchecked wrapper for {@link ReflectiveOperationException}, used to propagate reflective errors + * without requiring callers to declare or catch checked exceptions. + */ +public class RuntimeReflectiveOperationException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * Creates a new instance wrapping the given cause. + * + * @param cause the cause to wrap + */ + public RuntimeReflectiveOperationException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/ColumnConfiguration.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/ColumnConfiguration.java new file mode 100644 index 0000000..53a3d33 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/ColumnConfiguration.java @@ -0,0 +1,108 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.config; + +import com.flowingcode.vaadin.addons.easygrid.renderers.RendererFactory; +import com.vaadin.flow.component.grid.ColumnTextAlign; +import com.vaadin.flow.function.SerializableFunction; +import java.io.Serializable; + +/** + * Holds display configuration for a grid column value type, including text alignment, null + * representation, and renderer factory. Implementations chain through a parent configuration so + * that non-{@code null} values at a more specific level take precedence over less specific ones. + * + * @param the column value type + */ +public sealed interface ColumnConfiguration extends Serializable + permits ColumnConfigurationImpl, ColumnConfigurationLink { + + /** + * Sets the string to display when the column value is {@code null}. + * + * @param nullRepresentation the null representation string + * @return this configuration, for fluent chaining + */ + ColumnConfiguration setNullRepresentation(String nullRepresentation); + + /** + * Sets the text alignment for cells of this column type. + * + * @param textAlign the text alignment to apply + * @return this configuration, for fluent chaining + */ + ColumnConfiguration setTextAlign(ColumnTextAlign textAlign); + + /** + * Returns the effective text alignment, walking the configuration chain until a non-{@code null} + * value is found, or {@code null} if none is set. + * + * @return the effective text alignment, or {@code null} + */ + ColumnTextAlign getTextAlign(); + + /** + * Returns the effective null representation, walking the configuration chain until a + * non-{@code null} value is found, or {@code null} if none is set. + * + * @return the effective null representation, or {@code null} + */ + String getNullRepresentation(); + + /** + * Sets a formatter that converts a column value to a display string. Calling this method replaces + * any previously set renderer factory with a {@link com.vaadin.flow.data.renderer.TextRenderer} + * that applies the formatter. + * + * @param formatter a function mapping a column value to a display string + * @return this configuration, for fluent chaining + */ + ColumnConfiguration setFormatter(SerializableFunction formatter); + + /** + * Returns the effective renderer factory, walking the configuration chain until a + * non-{@code null} value is found, or {@code null} if none is configured. + * + * @param the grid bean type + * @return the effective renderer factory, or {@code null} + */ + RendererFactory getRendererFactory(); + + /** + * Sets a custom renderer factory that creates a renderer from a value provider. Replaces any + * previously set formatter or renderer factory. + * + * @param the grid bean type + * @param rendererFactory a factory that creates a renderer from a value provider + * @return this configuration, for fluent chaining + */ + ColumnConfiguration setRendererFactory(RendererFactory rendererFactory); + + /** + * Returns a new independent configuration node that inherits from this configuration. Values set + * on the returned node take precedence over this configuration's values; values not set on the + * returned node fall through to this configuration. + * + * @return a new {@code ColumnConfiguration} whose parent is {@code this} + */ + default ColumnConfiguration createNewLayer() { + return new ColumnConfigurationImpl<>(this); + } +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/ColumnConfigurationImpl.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/ColumnConfigurationImpl.java new file mode 100644 index 0000000..708c023 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/ColumnConfigurationImpl.java @@ -0,0 +1,92 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.config; + +import com.flowingcode.vaadin.addons.easygrid.renderers.RendererFactory; +import com.vaadin.flow.component.grid.ColumnTextAlign; +import com.vaadin.flow.data.renderer.Renderer; +import com.vaadin.flow.function.SerializableFunction; +import com.vaadin.flow.function.ValueProvider; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + * Holds display configuration for a grid column, including text alignment, null representation, + * and renderer factory. + * + *

Instances are obtained through the configuration-tree classes: + * {@link GlobalEasyGridConfiguration} and {@link InstanceEasyGridConfiguration}. + * + *

Configurations form a chain: each getter returns this instance's own value when + * non-{@code null}, otherwise delegates to the parent. + * + * @param the column value type + */ +@SuppressWarnings("serial") +@Setter +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +@Accessors(chain = true) +final class ColumnConfigurationImpl implements ColumnConfiguration { + + private final ColumnConfiguration parent; + + private ColumnTextAlign textAlign; + + private String nullRepresentation; + private RendererFactory, Renderer> rendererFactory; + + @Override + public ColumnTextAlign getTextAlign() { + return textAlign != null ? textAlign + : (parent != null ? parent.getTextAlign() : null); + } + + @Override + public String getNullRepresentation() { + return nullRepresentation != null ? nullRepresentation + : (parent != null ? parent.getNullRepresentation() : null); + } + + @Override + public RendererFactory getRendererFactory() { + @SuppressWarnings({"unchecked", "rawtypes"}) + RendererFactory factory = (RendererFactory) rendererFactory; + if (factory == null && parent != null) { + factory = parent.getRendererFactory(); + } + return factory; + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public ColumnConfiguration setRendererFactory(RendererFactory rendererFactory) { + this.rendererFactory = (RendererFactory) rendererFactory; + return this; + } + + @Override + public ColumnConfiguration setFormatter(SerializableFunction formatter) { + setRendererFactory(getter -> new ColumnConfigurationTextRenderer<>(getter, this, formatter)); + return this; + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/ColumnConfigurationLink.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/ColumnConfigurationLink.java new file mode 100644 index 0000000..bbd0b27 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/ColumnConfigurationLink.java @@ -0,0 +1,92 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.config; + +import com.flowingcode.vaadin.addons.easygrid.renderers.RendererFactory; +import com.vaadin.flow.component.grid.ColumnTextAlign; +import com.vaadin.flow.function.SerializableFunction; +import java.util.function.Function; +import lombok.AccessLevel; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +/** + * A {@code ColumnConfiguration} that delegates reads to a primary configuration and, when the + * primary returns {@code null} for a given property, falls back to a secondary configuration. + * Writes (setters) are always forwarded to the primary and return {@code this} for fluent chaining. + */ +@SuppressWarnings("serial") +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +final class ColumnConfigurationLink implements ColumnConfiguration { + + @NonNull + private final ColumnConfiguration primary; + + /** Secondary configuration consulted when the primary returns {@code null} for a property. */ + private final ColumnConfiguration fallback; + + private T get(Function, T> f) { + T t = f.apply(primary); + if (t == null && fallback != null) { + t = f.apply(fallback); + } + return t; + } + + @Override + public ColumnConfiguration setNullRepresentation(String nullRepresentation) { + primary.setNullRepresentation(nullRepresentation); + return this; + } + + @Override + public ColumnConfiguration setTextAlign(ColumnTextAlign textAlign) { + primary.setTextAlign(textAlign); + return this; + } + + @Override + public ColumnConfiguration setFormatter(SerializableFunction formatter) { + primary.setFormatter(formatter); + return this; + } + + @Override + public ColumnConfiguration setRendererFactory(RendererFactory rendererFactory) { + primary.setRendererFactory(rendererFactory); + return this; + } + + @Override + public ColumnTextAlign getTextAlign() { + return get(ColumnConfiguration::getTextAlign); + } + + @Override + public String getNullRepresentation() { + return get(ColumnConfiguration::getNullRepresentation); + } + + @Override + public RendererFactory getRendererFactory() { + return get(ColumnConfiguration::getRendererFactory); + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/ColumnConfigurationTextRenderer.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/ColumnConfigurationTextRenderer.java new file mode 100644 index 0000000..1e9ffc3 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/ColumnConfigurationTextRenderer.java @@ -0,0 +1,72 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.config; + +import com.vaadin.flow.data.renderer.TextRenderer; +import com.vaadin.flow.function.SerializableFunction; +import com.vaadin.flow.function.ValueProvider; + +/** + * A {@code TextRenderer} that formats column values using a + * {@link ColumnConfiguration}. When the value is {@code null}, the configuration's null + * representation is used. Otherwise, an optional formatter is applied; if none is provided, + * {@link Object#toString()} is called. + * + * @param the grid bean type + * @param the column value type + */ +@SuppressWarnings("serial") +public final class ColumnConfigurationTextRenderer extends TextRenderer { + + /** + * Creates a renderer that applies the given formatter to non-{@code null} values and returns + * the configuration's null representation for {@code null} values. + * + * @param getter a value provider that extracts the column value from a row item + * @param config the column configuration supplying the null representation + * @param formatter a function mapping a value and its configuration to a display string, or + * {@code null} to fall back to {@link Object#toString()} + */ + public ColumnConfigurationTextRenderer(ValueProvider getter, ColumnConfiguration config, + SerializableFunction formatter) { + super(item -> { + V val = getter.apply(item); + if (val == null) { + return config.getNullRepresentation(); + } + if (formatter != null) { + return formatter.apply(val); + } + return val.toString(); + }); + } + + /** + * Creates a renderer that falls back to {@link Object#toString()} for non-{@code null} values + * and returns the configuration's null representation for {@code null} values. + * + * @param getter a value provider that extracts the column value from a row item + * @param config the column configuration supplying the null representation + */ + public ColumnConfigurationTextRenderer(ValueProvider getter, ColumnConfiguration config) { + this(getter, config, null); + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/EasyGridConfigurationClassMap.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/EasyGridConfigurationClassMap.java new file mode 100644 index 0000000..069d02d --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/EasyGridConfigurationClassMap.java @@ -0,0 +1,91 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.config; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * A map from value type to {@link ColumnConfiguration}, building a parent chain by following the + * class hierarchy so that a configuration for a subtype inherits from its supertype's configuration. + * Primitive types are mapped to their wrapper counterparts before hierarchy traversal. + */ +@SuppressWarnings("serial") +final class EasyGridConfigurationClassMap implements Serializable { + + private final Map, ColumnConfiguration> map = new HashMap<>(); + + @SuppressWarnings("unchecked") + public synchronized ColumnConfiguration getOrCreate(Class type) { + return Optional.ofNullable((ColumnConfiguration) map.get(type)).orElseGet(()->{ + var supertype = getSuperclass(type); + ColumnConfiguration parent = + supertype != null ? (ColumnConfiguration) getOrCreate(supertype) : null; + ColumnConfiguration config = new ColumnConfigurationImpl<>(parent); + map.put(type, config); + return config; + }); + } + + private Class getSuperclass(Class type) { + if (type.isPrimitive()) { + if (type == boolean.class) { + return Boolean.class; + } + if (type == byte.class) { + return Byte.class; + } + if (type == short.class) { + return Short.class; + } + if (type == int.class) { + return Integer.class; + } + if (type == long.class) { + return Long.class; + } + if (type == float.class) { + return Float.class; + } + if (type == double.class) { + return Double.class; + } + if (type == char.class) { + return Character.class; + } + throw new IllegalArgumentException(type.getName()); + } + return type.getSuperclass(); + } + + @SuppressWarnings("unchecked") + public synchronized ColumnConfiguration get(Class type) { + for (Class c = type; c != null; c = getSuperclass(c)) { + ColumnConfiguration config = map.get(c); + if (config != null) { + return (ColumnConfiguration) config; + } + } + return null; + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/GlobalEasyGridConfiguration.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/GlobalEasyGridConfiguration.java new file mode 100644 index 0000000..97b3031 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/GlobalEasyGridConfiguration.java @@ -0,0 +1,108 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.config; + +import com.flowingcode.vaadin.addons.easygrid.EasyGrid; +import com.flowingcode.vaadin.addons.easygrid.renderers.LocalDateRenderers; +import com.flowingcode.vaadin.addons.easygrid.renderers.LocalDateTimeRenderers; +import com.flowingcode.vaadin.addons.easygrid.renderers.LocalTimeRenderers; +import com.flowingcode.vaadin.addons.easygrid.renderers.NumberRenderers; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.grid.ColumnTextAlign; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import lombok.Getter; +import lombok.experimental.UtilityClass; + +/** + * System-wide {@link EasyGrid} configuration. Configurations registered here apply to all + * {@code EasyGrid} instances across all sessions. + * + *

Use {@link #forType(Class)} to obtain a configuration for a given type: + * + *

+ * GlobalEasyGridConfiguration.forType(LocalDate.class).setRendererFactory(...);
+ * 
+ * + *

Startup and thread safety: This class is a mutable static singleton shared + * across all user sessions. All calls to {@link #forType(Class)} must be made during application + * startup, before any session is created, to avoid data races between concurrent sessions. Call + * {@link #freeze()} at the end of startup to lock the configuration and prevent accidental + * post-startup modifications. Omitting {@link #freeze()} is a stability risk: any code that runs + * after startup — including request-handling code — could inadvertently alter rendering for every + * active session. + */ +@UtilityClass +public class GlobalEasyGridConfiguration { + + private static final EasyGridConfigurationClassMap map = new EasyGridConfigurationClassMap(); + + @Getter + private static volatile boolean frozen; + + static { + forType(Object.class).setNullRepresentation(""); + forType(Boolean.class).setTextAlign(ColumnTextAlign.CENTER); + forType(Number.class).setTextAlign(ColumnTextAlign.END); + forType(Number.class).setRendererFactory(NumberRenderers.of(() -> UI.getCurrent().getLocale())); + forType(LocalDate.class).setRendererFactory(LocalDateRenderers.of("yyyy-MM-dd")); + forType(LocalDateTime.class).setRendererFactory(LocalDateTimeRenderers.of("yyyy-MM-dd HH:mm:ss")); + forType(LocalTime.class).setRendererFactory(LocalTimeRenderers.of("HH:mm:ss")); + } + + /** + * Freezes the global configuration, preventing further calls to {@link #forType(Class)}, which + * will throw {@link IllegalStateException} once frozen. Freezing is irreversible. This method + * should be called at the end of application startup, after all type configurations have been + * registered. + */ + public static void freeze() { + frozen = true; + } + + /** + * Returns the {@code ColumnConfiguration} for the given type at the global level, creating it if + * it does not yet exist. Modifications to the returned configuration take effect immediately. + * + * @param type the column value type + * @return the {@code ColumnConfiguration} for the given type + * @throws IllegalStateException if the global configuration has been frozen via {@link #freeze()} + */ + public static ColumnConfiguration forType(Class type) { + if (frozen) { + throw new IllegalStateException("Global configuration is frozen and no longer accepts registrations"); + } + return map.getOrCreate(type); + } + + /** + * Returns the effective {@code ColumnConfiguration} for the given type. When frozen, returns + * the nearest registered configuration walking the class hierarchy, or {@code null} if none. + * When not frozen, creates a configuration if one does not yet exist. + * + * @param type the column value type + * @return the resolved configuration, or {@code null} when frozen and no config was registered + */ + ColumnConfiguration resolve(Class type) { + return frozen ? map.get(type) : forType(type); + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/InstanceEasyGridConfiguration.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/InstanceEasyGridConfiguration.java new file mode 100644 index 0000000..53c673f --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/config/InstanceEasyGridConfiguration.java @@ -0,0 +1,69 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.config; + +import com.flowingcode.vaadin.addons.easygrid.EasyGrid; +import java.io.Serializable; + +/** + * Instance-level {@link EasyGrid} configuration. Configurations registered here apply only to the + * specific {@code EasyGrid} instance that holds this object and take precedence over + * {@link GlobalEasyGridConfiguration}. + * + *

See {@link #resolve(Class)} for the full resolution order. + */ +@SuppressWarnings("serial") +public final class InstanceEasyGridConfiguration implements Serializable { + + private final EasyGridConfigurationClassMap byTypeConfigurations = + new EasyGridConfigurationClassMap(); + + /** + * Returns the {@code ColumnConfiguration} for the given type at the instance level, creating it + * if it does not yet exist. Modifications to the returned configuration take effect immediately. + * + * @param type the column value type + * @return the {@code ColumnConfiguration} for the given type + */ + public ColumnConfiguration forType(Class type) { + return link(byTypeConfigurations.getOrCreate(type), type); + } + + private ColumnConfiguration link(ColumnConfiguration config, Class type) { + return new ColumnConfigurationLink(config, GlobalEasyGridConfiguration.resolve(type)); + } + + /** + * Returns the effective {@code ColumnConfiguration} for the given type, chaining configurations + * across all levels of the tree. The resolution order, from most to least specific, is: + *

    + *
  1. Type-level configuration registered on this instance via {@link #forType(Class)}.
  2. + *
  3. Type-level configuration registered on {@link GlobalEasyGridConfiguration}.
  4. + *
+ * Each level's non-{@code null} fields take precedence over the levels below it. + * + * @param type the column value type + * @return the resolved configuration, never {@code null} + */ + public ColumnConfiguration resolve(Class type) { + return forType(type).createNewLayer(); + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/renderers/LocalDateRenderers.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/renderers/LocalDateRenderers.java new file mode 100644 index 0000000..2a8ceeb --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/renderers/LocalDateRenderers.java @@ -0,0 +1,102 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.renderers; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import com.vaadin.flow.data.renderer.LocalDateRenderer; +import com.vaadin.flow.function.SerializableSupplier; +import lombok.experimental.UtilityClass; + +/** + * Factory methods for creating {@link RendererFactory} instances that render {@link LocalDate} + * values using {@link com.vaadin.flow.data.renderer.LocalDateRenderer}. + */ +@UtilityClass +public class LocalDateRenderers { + + /** + * Returns a factory that uses the default locale and format. + * + * @return a {@code RendererFactory} using the default locale and format + */ + public RendererFactory of() { + return getter -> new LocalDateRenderer<>(getter); + } + + /** + * Returns a factory that formats dates with the given pattern using the default locale. + * + * @param formatPattern a {@link DateTimeFormatter} pattern + * @return a {@code RendererFactory} for the given pattern + */ + public RendererFactory of(String formatPattern) { + return getter -> new LocalDateRenderer<>(getter, formatPattern); + } + + /** + * Returns a factory that formats dates with the given pattern and locale. + * + * @param formatPattern a {@link DateTimeFormatter} pattern + * @param locale the locale to use + * @return a {@code RendererFactory} for the given pattern and locale + */ + public RendererFactory of(String formatPattern, Locale locale) { + return getter -> new LocalDateRenderer<>(getter, formatPattern, locale); + } + + /** + * Returns a factory that formats dates with the given pattern, locale, and null representation. + * + * @param formatPattern a {@link DateTimeFormatter} pattern + * @param locale the locale to use + * @param nullRepresentation the string to display for {@code null} values + * @return a {@code RendererFactory} for the given pattern, locale, and null representation + */ + public RendererFactory of(String formatPattern, Locale locale, + String nullRepresentation) { + return getter -> new LocalDateRenderer<>(getter, formatPattern, locale, nullRepresentation); + } + + /** + * Returns a factory that formats dates using the given {@link DateTimeFormatter} supplier. + * + * @param formatter a supplier of the formatter to use + * @return a {@code RendererFactory} using the given formatter supplier + */ + public RendererFactory of(SerializableSupplier formatter) { + return getter -> new LocalDateRenderer<>(getter, formatter); + } + + /** + * Returns a factory that formats dates using the given {@link DateTimeFormatter} supplier and + * null representation. + * + * @param formatter a supplier of the formatter to use + * @param nullRepresentation the string to display for {@code null} values + * @return a {@code RendererFactory} using the given formatter supplier and null representation + */ + public RendererFactory of(SerializableSupplier formatter, + String nullRepresentation) { + return getter -> new LocalDateRenderer<>(getter, formatter, nullRepresentation); + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/renderers/LocalDateTimeRenderers.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/renderers/LocalDateTimeRenderers.java new file mode 100644 index 0000000..36e1a72 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/renderers/LocalDateTimeRenderers.java @@ -0,0 +1,104 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.renderers; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import com.vaadin.flow.data.renderer.LocalDateTimeRenderer; +import com.vaadin.flow.function.SerializableSupplier; +import lombok.experimental.UtilityClass; + +/** + * Factory methods for creating {@link RendererFactory} instances that render {@link LocalDateTime} + * values using {@link com.vaadin.flow.data.renderer.LocalDateTimeRenderer}. + */ +@UtilityClass +public class LocalDateTimeRenderers { + + /** + * Returns a factory that uses the default locale and format. + * + * @return a {@code RendererFactory} using the default locale and format + */ + public RendererFactory of() { + return getter -> new LocalDateTimeRenderer<>(getter); + } + + /** + * Returns a factory that formats date-times with the given pattern using the default locale. + * + * @param formatPattern a {@link DateTimeFormatter} pattern + * @return a {@code RendererFactory} for the given pattern + */ + public RendererFactory of(String formatPattern) { + return getter -> new LocalDateTimeRenderer<>(getter, formatPattern); + } + + /** + * Returns a factory that formats date-times with the given pattern and locale. + * + * @param formatPattern a {@link DateTimeFormatter} pattern + * @param locale the locale to use + * @return a {@code RendererFactory} for the given pattern and locale + */ + public RendererFactory of(String formatPattern, Locale locale) { + return getter -> new LocalDateTimeRenderer<>(getter, formatPattern, locale); + } + + /** + * Returns a factory that formats date-times with the given pattern, locale, and null + * representation. + * + * @param formatPattern a {@link DateTimeFormatter} pattern + * @param locale the locale to use + * @param nullRepresentation the string to display for {@code null} values + * @return a {@code RendererFactory} for the given pattern, locale, and null representation + */ + public RendererFactory of(String formatPattern, Locale locale, + String nullRepresentation) { + return getter -> new LocalDateTimeRenderer<>(getter, formatPattern, locale, nullRepresentation); + } + + /** + * Returns a factory that formats date-times using the given {@link DateTimeFormatter} supplier. + * + * @param formatter a supplier of the formatter to use + * @return a {@code RendererFactory} using the given formatter supplier + */ + public RendererFactory of( + SerializableSupplier formatter) { + return getter -> new LocalDateTimeRenderer<>(getter, formatter); + } + + /** + * Returns a factory that formats date-times using the given {@link DateTimeFormatter} supplier + * and null representation. + * + * @param formatter a supplier of the formatter to use + * @param nullRepresentation the string to display for {@code null} values + * @return a {@code RendererFactory} using the given formatter supplier and null representation + */ + public RendererFactory of( + SerializableSupplier formatter, String nullRepresentation) { + return getter -> new LocalDateTimeRenderer<>(getter, formatter, nullRepresentation); + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/renderers/LocalTimeRenderers.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/renderers/LocalTimeRenderers.java new file mode 100644 index 0000000..c34a3e6 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/renderers/LocalTimeRenderers.java @@ -0,0 +1,106 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.renderers; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import com.vaadin.flow.data.renderer.TextRenderer; +import com.vaadin.flow.function.SerializableSupplier; +import lombok.experimental.UtilityClass; + +/** + * Factory methods for creating {@link RendererFactory} instances that render {@link LocalTime} + * values using {@link TextRenderer} and {@link DateTimeFormatter}. + */ +@UtilityClass +public class LocalTimeRenderers { + + /** + * Returns a factory that uses {@link DateTimeFormatter#ISO_LOCAL_TIME} and the default locale. + * + * @return a {@code RendererFactory} using {@code DateTimeFormatter#ISO_LOCAL_TIME} and the + * default locale + */ + public RendererFactory of() { + return of(() -> DateTimeFormatter.ISO_LOCAL_TIME); + } + + /** + * Returns a factory that formats times with the given pattern using the default locale. + * + * @param formatPattern a {@link DateTimeFormatter} pattern + * @return a {@code RendererFactory} for the given pattern + */ + public RendererFactory of(String formatPattern) { + return of(() -> DateTimeFormatter.ofPattern(formatPattern)); + } + + /** + * Returns a factory that formats times with the given pattern and locale. + * + * @param formatPattern a {@link DateTimeFormatter} pattern + * @param locale the locale to use + * @return a {@code RendererFactory} for the given pattern and locale + */ + public RendererFactory of(String formatPattern, Locale locale) { + return of(() -> DateTimeFormatter.ofPattern(formatPattern, locale)); + } + + /** + * Returns a factory that formats times with the given pattern, locale, and null representation. + * + * @param formatPattern a {@link DateTimeFormatter} pattern + * @param locale the locale to use + * @param nullRepresentation the string to display for {@code null} values + * @return a {@code RendererFactory} for the given pattern, locale, and null representation + */ + public RendererFactory of(String formatPattern, Locale locale, + String nullRepresentation) { + return of(() -> DateTimeFormatter.ofPattern(formatPattern, locale), nullRepresentation); + } + + /** + * Returns a factory that formats times using the given {@link DateTimeFormatter} supplier. + * + * @param formatter a supplier of the formatter to use + * @return a {@code RendererFactory} using the given formatter supplier + */ + public RendererFactory of(SerializableSupplier formatter) { + return of(formatter, ""); + } + + /** + * Returns a factory that formats times using the given {@link DateTimeFormatter} supplier and + * null representation. + * + * @param formatter a supplier of the formatter to use + * @param nullRepresentation the string to display for {@code null} values + * @return a {@code RendererFactory} using the given formatter supplier and null representation + */ + public RendererFactory of(SerializableSupplier formatter, + String nullRepresentation) { + return getter -> new TextRenderer<>(item -> { + LocalTime val = getter.apply(item); + return val != null ? formatter.get().format(val) : nullRepresentation; + }); + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/renderers/NumberRenderers.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/renderers/NumberRenderers.java new file mode 100644 index 0000000..b089830 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/renderers/NumberRenderers.java @@ -0,0 +1,130 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.renderers; + +import java.text.NumberFormat; +import java.util.Locale; +import com.vaadin.flow.data.renderer.NumberRenderer; +import com.vaadin.flow.function.SerializableSupplier; +import lombok.experimental.UtilityClass; + +/** + * Factory methods for creating {@link RendererFactory} instances that render {@link Number} values + * using {@link com.vaadin.flow.data.renderer.NumberRenderer}. + */ +@UtilityClass +public class NumberRenderers { + + /** + * Returns a factory that formats numbers using the given {@code NumberFormat}. + * + * @param numberFormat the format to use + * @return a {@code RendererFactory} for the given format + */ + public RendererFactory of(NumberFormat numberFormat) { + return getter -> new NumberRenderer<>(getter, numberFormat); + } + + /** + * Returns a factory that formats numbers using the given {@code NumberFormat} and null + * representation. + * + * @param numberFormat the format to use + * @param nullRepresentation the string to display for {@code null} values + * @return a {@code RendererFactory} for the given format and null representation + */ + public RendererFactory of(NumberFormat numberFormat, String nullRepresentation) { + return getter -> new NumberRenderer<>(getter, numberFormat, nullRepresentation); + } + + /** + * Returns a factory that formats numbers using the default format for the given locale. + * + * @param locale the locale to use + * @return a {@code RendererFactory} using the default format for the given locale + */ + public RendererFactory of(Locale locale) { + return getter -> new NumberRenderer<>(getter, locale); + } + + /** + * Returns a factory that formats numbers using the default format for the locale supplied at + * column-creation time. + * + * @param locale a supplier of the locale to use + * @return a {@code RendererFactory} using the default format for the supplied locale + */ + public RendererFactory of(SerializableSupplier locale) { + return getter -> new NumberRenderer<>(getter, locale.get()); + } + + /** + * Returns a factory that formats numbers using the default format for the locale supplied at + * column-creation time, with the given null representation. + * + * @param locale a supplier of the locale to use + * @param nullRepresentation the string to display for {@code null} values + * @return a {@code RendererFactory} using the default format for the supplied locale and the + * given null representation + */ + public RendererFactory of(SerializableSupplier locale, + String nullRepresentation) { + return getter -> new NumberRenderer<>(getter, NumberFormat.getInstance(locale.get()), + nullRepresentation); + } + + /** + * Returns a factory that formats numbers using the given {@link java.util.Formatter} format + * string and the default locale. + * + * @param formatString a {@code java.util.Formatter} format string + * @return a {@code RendererFactory} for the given format string + */ + public RendererFactory of(String formatString) { + return getter -> new NumberRenderer<>(getter, formatString); + } + + /** + * Returns a factory that formats numbers using the given {@link java.util.Formatter} format + * string and locale. + * + * @param formatString a {@code Formatter} format string + * @param locale the locale to use + * @return a {@code RendererFactory} for the given format string and locale + */ + public RendererFactory of(String formatString, Locale locale) { + return getter -> new NumberRenderer<>(getter, formatString, locale); + } + + /** + * Returns a factory that formats numbers using the given {@link java.util.Formatter} format + * string, locale, and null representation. + * + * @param formatString a {@code Formatter} format string + * @param locale the locale to use + * @param nullRepresentation the string to display for {@code null} values + * @return a {@code RendererFactory} for the given format string, locale, and null representation + */ + public RendererFactory of(String formatString, Locale locale, + String nullRepresentation) { + return getter -> new NumberRenderer<>(getter, formatString, locale, nullRepresentation); + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/renderers/RendererFactory.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/renderers/RendererFactory.java new file mode 100644 index 0000000..dac9cfa --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/renderers/RendererFactory.java @@ -0,0 +1,36 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.renderers; + +import com.vaadin.flow.data.renderer.Renderer; +import com.vaadin.flow.function.SerializableFunction; +import com.vaadin.flow.function.ValueProvider; + +/** + * A named functional interface that creates a {@link Renderer} for a grid column given a + * {@link com.vaadin.flow.function.ValueProvider}. + * + * @param the grid bean type + * @param the column value type + */ +@FunctionalInterface +public interface RendererFactory extends SerializableFunction, Renderer> { + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java b/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java index 7110add..f00a909 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java +++ b/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java @@ -2,14 +2,14 @@ * #%L * Easy Grid Add-on * %% - * Copyright (C) 2023 Flowing Code + * Copyright (C) 2020 - 2026 Flowing Code * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/AutoColumnsDemo.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/AutoColumnsDemo.java new file mode 100644 index 0000000..cd1ceca --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/AutoColumnsDemo.java @@ -0,0 +1,61 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid; + +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.flowingcode.vaadin.addons.easygrid.model.Person; +import com.flowingcode.vaadin.addons.easygrid.service.PersonService; +import com.vaadin.flow.component.Html; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; + +@DemoSource +@PageTitle("Auto Column Discovery") +@SuppressWarnings("serial") +@Route(value = "easy-grid/auto-columns", layout = EasyGridDemoView.class) +public class AutoColumnsDemo extends Div { + + private final PersonService service = new PersonService(); + + //#if vaadin eq 0 + private final Html description = new Html(""" +
All bean properties of the given class are discovered automatically. +
    +
  • Column headers are derived from camelCase names (e.g. firstName → "First Name").
  • +
  • Columns whose type implements Comparable, or that are primitive, are sortable.
  • +
+
+ """); + //#endif + + public AutoColumnsDemo() { + + // All bean properties are discovered automatically from the class. + // Headers are derived from camelCase names ("firstName" → "First Name"). + // Columns whose type implements Comparable, or that are primitive, are made sortable. + var grid = new EasyGrid<>(Person.class); + + grid.setItems(service.fetchAll()); + add(grid); + add(description); //hide-source + setSizeFull(); + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/ColumnConfigurationDemo.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/ColumnConfigurationDemo.java new file mode 100644 index 0000000..5b431b1 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/ColumnConfigurationDemo.java @@ -0,0 +1,88 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid; + +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.flowingcode.vaadin.addons.easygrid.model.Person; +import com.flowingcode.vaadin.addons.easygrid.renderers.LocalDateRenderers; +import com.flowingcode.vaadin.addons.easygrid.renderers.LocalDateTimeRenderers; +import com.flowingcode.vaadin.addons.easygrid.service.PersonService; +import com.vaadin.flow.component.Html; +import com.vaadin.flow.component.grid.ColumnTextAlign; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.Getter; + +@DemoSource +@PageTitle("Column Configuration") +@SuppressWarnings("serial") +@Route(value = "easy-grid/column-config", layout = EasyGridDemoView.class) +@Getter // hide-source +public class ColumnConfigurationDemo extends Div { + + private final PersonService service = new PersonService(); + + //#if vaadin eq 0 + private final Html description = new Html(""" +
Demonstrates selective column creation by listing property names in the constructor. +
    +
  • Column properties are configured via Grid.Column delegation + (setHeader, setWidth, setFlexGrow).
  • +
  • The .as(Type.class) method narrows an EasyColumn to a typed handle + for type-specific configuration such as setRendererFactory, + setFormatter, and setNullRepresentation.
  • +
+
+ """); + //#endif + + public ColumnConfigurationDemo() { + + EasyGrid grid = new EasyGrid<>(Person.class, + "firstName", "lastName", "birthDate", "appointmentDateTime", "subscriber", "age"); + + // Grid.Column delegation — setHeader, setWidth, setFlexGrow, etc. are forwarded directly + grid.getColumn("birthDate").setHeader("Date of Birth"); + grid.getColumn("appointmentDateTime").setHeader("Appointment"); + grid.getColumn("lastName").setWidth("180px").setFlexGrow(0); + + // .as(Type.class) narrows the EasyColumn to a typed handle for type-specific methods + grid.getColumn("birthDate").as(LocalDate.class) + .setRendererFactory(LocalDateRenderers.of("dd/MM/yyyy")); + + grid.getColumn("appointmentDateTime").as(LocalDateTime.class) + .setRendererFactory(LocalDateTimeRenderers.of("dd/MM/yyyy HH:mm")) + .setNullRepresentation("—"); + + grid.getColumn("subscriber").as(Boolean.class) + .setFormatter(v -> v ? "Subscribed" : "Not Subscribed"); + + // setTextAlign overrides the alignment set by the type-level configuration + grid.getColumn("age").setTextAlign(ColumnTextAlign.CENTER); + + grid.setItems(service.fetchAll()); + add(grid); + add(description); //hide-source + setSizeFull(); + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/ConfigurationHierarchyDemo.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/ConfigurationHierarchyDemo.java new file mode 100644 index 0000000..c389ab3 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/ConfigurationHierarchyDemo.java @@ -0,0 +1,84 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid; + +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.flowingcode.vaadin.addons.easygrid.config.GlobalEasyGridConfiguration; +import com.flowingcode.vaadin.addons.easygrid.model.Address; +import com.flowingcode.vaadin.addons.easygrid.model.Person; +import com.flowingcode.vaadin.addons.easygrid.service.PersonService; +import com.vaadin.flow.component.Html; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import lombok.Getter; + +@DemoSource +@PageTitle("Configuration Hierarchy") +@SuppressWarnings("serial") +@Route(value = "easy-grid/config-hierarchy", layout = EasyGridDemoView.class) +@Getter // hide-source +public class ConfigurationHierarchyDemo extends Div { + + private final PersonService service = new PersonService(); + + //#if vaadin eq 0 + private final Html description = new Html(""" +
Configuration is resolved by precedence: property level (highest) → instance type level → global level (lowest). +
    +
  • The address column uses the global Address formatter.
  • +
  • The active column uses the instance-level Boolean formatter.
  • +
  • subscriber overrides both with its own property-level formatter.
  • +
+
+ """); + //#endif + + public ConfigurationHierarchyDemo() { + + // Level 3 (lowest) — global: applies to every EasyGrid instance in the application. + // Registered once at startup; affects all grids that show an Address column. + GlobalEasyGridConfiguration.forType(Address.class) + .setFormatter(a -> a.getPostalCode() + " " + a.getCity()); + + EasyGrid grid = new EasyGrid<>(Person.class, + "firstName", "subscriber", "active", "address"); + + // Level 2 — instance type: overrides the global Boolean default ("true"/"false") + // for all Boolean columns in this EasyGrid only. + grid.typeConfiguration(Boolean.class) + .setFormatter(v -> v ? "Yes" : "No"); + + // Level 1 (highest) — property: overrides the instance type config for "subscriber" only. + // Any other Boolean column in this grid would still show "Yes" / "No" (instance level). + grid.getColumn("subscriber").as(Boolean.class) + .setFormatter(v -> v ? "Subscribed" : "Not Subscribed"); + + // Resolution summary for this grid: + // address → global Address formatter ("postalCode city") + // subscriber → property-level formatter ("Subscribed" / "Not Subscribed") + // (any other Boolean column would use instance-level "Yes" / "No") + + grid.setItems(service.fetchAll()); + add(grid); + add(description); //hide-source + setSizeFull(); + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/DemoView.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/DemoView.java index cd95e9d..0c98166 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/easygrid/DemoView.java +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/DemoView.java @@ -2,14 +2,14 @@ * #%L * Easy Grid Add-on * %% - * Copyright (C) 2023 Flowing Code + * Copyright (C) 2020 - 2026 Flowing Code * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/EasyGridDemo.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/EasyGridDemo.java deleted file mode 100644 index adf2125..0000000 --- a/src/test/java/com/flowingcode/vaadin/addons/easygrid/EasyGridDemo.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.flowingcode.vaadin.addons.easygrid; - -import com.flowingcode.vaadin.addons.demo.DemoSource; -import com.vaadin.flow.component.html.Div; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; - -@DemoSource -@PageTitle("Easy Grid Add-on Demo") -@SuppressWarnings("serial") -@Route(value = "demo", layout = EasyGridDemoView.class) -public class EasyGridDemo extends Div { - - public EasyGridDemo() { - add(new EasyGridAddon()); - } -} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/EasyGridDemoView.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/EasyGridDemoView.java index b3a8a65..a8f232d 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/easygrid/EasyGridDemoView.java +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/EasyGridDemoView.java @@ -2,14 +2,14 @@ * #%L * Easy Grid Add-on * %% - * Copyright (C) 2023 Flowing Code + * Copyright (C) 2020 - 2026 Flowing Code * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,6 +22,7 @@ import com.flowingcode.vaadin.addons.DemoLayout; import com.flowingcode.vaadin.addons.GithubLink; import com.flowingcode.vaadin.addons.demo.TabbedDemo; +import com.vaadin.flow.component.dependency.CssImport; import com.vaadin.flow.router.ParentLayout; import com.vaadin.flow.router.Route; @@ -29,10 +30,17 @@ @ParentLayout(DemoLayout.class) @Route("easy-grid") @GithubLink("https://github.com/FlowingCode/EasyGridAddon") +@CssImport("./styles/easy-grid-demo-styles.css") public class EasyGridDemoView extends TabbedDemo { public EasyGridDemoView() { - addDemo(EasyGridDemo.class); + addDemo(AutoColumnsDemo.class); + addDemo(SelectiveColumnsDemo.class); + addDemo(TypeRenderingDemo.class); + addDemo(NumberRenderingDemo.class); + addDemo(ColumnConfigurationDemo.class); + addDemo(ConfigurationHierarchyDemo.class); + addDemo(TypedColumnDemo.class); setSizeFull(); } } diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/GridColumnMethodLister.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/GridColumnMethodLister.java new file mode 100644 index 0000000..82e6b66 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/GridColumnMethodLister.java @@ -0,0 +1,61 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid; + +import com.vaadin.flow.component.grid.Grid; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Prints all public methods declared on {@link Grid.Column}, sorted by name. + * + *

Each line has the form: + *

+ *   returnType methodName(paramType1, paramType2, ...)
+ * 
+ * + *

Run via {@code mvn exec:java -Dexec.mainClass=...GridColumnMethodLister} or directly from an + * IDE. + */ +public class GridColumnMethodLister { + + public static void main(String[] args) { + Set declaredPublic = Arrays.stream(Grid.Column.class.getDeclaredMethods()) + .filter(m -> Modifier.isPublic(m.getModifiers())) + .collect(Collectors.toSet()); + + declaredPublic.stream() + .sorted(Comparator.comparing(Method::getName) + .thenComparingInt(Method::getParameterCount)) + .forEach(GridColumnMethodLister::print); + } + + private static void print(Method m) { + String params = Arrays.stream(m.getParameterTypes()) + .map(Class::getSimpleName) + .collect(Collectors.joining(", ")); + System.out.printf("%s :: %s(%s)%n", m.getDeclaringClass().getSimpleName(), m.getName(), params); + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/NumberRenderingDemo.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/NumberRenderingDemo.java new file mode 100644 index 0000000..3e77ff6 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/NumberRenderingDemo.java @@ -0,0 +1,78 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid; + +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.flowingcode.vaadin.addons.easygrid.model.NumberSample; +import com.vaadin.flow.component.Html; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import java.math.BigDecimal; +import java.util.List; + +@DemoSource +@DemoSource(clazz = NumberSample.class) +@PageTitle("Number Rendering") +@SuppressWarnings("serial") +@Route(value = "easy-grid/number-rendering", layout = EasyGridDemoView.class) +public class NumberRenderingDemo extends Div { + + //#if vaadin eq 0 + private final Html description = new Html(""" +

Demonstrates number rendering for all supported numeric types + using locale-appropriate grouping separators. +
    +
  • Supported types: BigDecimal, BigInteger, Integer, + int, Long, Double.
  • +
  • Values that Double.toString() would display in scientific notation + (magnitude ≥ 1E7 or absolute value < 1E-3) are rendered in plain decimal notation.
  • +
+
+ """); + //#endif + + // Each row holds the same value expressed across all numeric types. + // Rows are chosen to exercise values that Double.toString() would render in scientific notation + // (magnitude >= 1E7 or absolute value < 1E-3), as well as small ordinary values. + private static final List ITEMS = List.of( + new NumberSample(new BigDecimal("1234567890123.456")), + new NumberSample(new BigDecimal("2147483647")), + new NumberSample(new BigDecimal("-5432100000")), + new NumberSample(new BigDecimal("0.000012345")), + new NumberSample(new BigDecimal("42.5")), + new NumberSample(new BigDecimal("0.42")) + ); + + public NumberRenderingDemo() { + + // All columns use the NumberRenderer wired by GlobalEasyGridConfiguration for Number.class. + // Values that Double.toString() would show in scientific notation are rendered with + // locale-appropriate grouping separators instead. + var grid = new EasyGrid<>(NumberSample.class, + "bigDecimal", "bigInteger", "integer", "intValue", "longValue", "doubleValue"); + + grid.setItems(ITEMS); + add(grid); + add(description); //hide-source + setSizeFull(); + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/SelectiveColumnsDemo.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/SelectiveColumnsDemo.java new file mode 100644 index 0000000..6987fb8 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/SelectiveColumnsDemo.java @@ -0,0 +1,77 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid; + +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.flowingcode.vaadin.addons.easygrid.model.Person; +import com.flowingcode.vaadin.addons.easygrid.service.PersonService; +import com.vaadin.flow.component.Html; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import lombok.Getter; + +@DemoSource +@PageTitle("Selective Columns") +@SuppressWarnings("serial") +@Route(value = "easy-grid/selective-columns", layout = EasyGridDemoView.class) +@Getter // hide-source +public class SelectiveColumnsDemo extends Div { + + private final PersonService service = new PersonService(); + + //#if vaadin eq 0 + private final Html description = new Html(""" +
Columns are created for explicit property names passed to the constructor. +
    +
  • Dot notation accesses nested bean properties (e.g. address.city).
  • +
  • Columns can be added after construction with addColumn/addColumns.
  • +
  • setColumnOrder reorders columns and hides any not listed.
  • +
  • hideColumns hides specific columns without affecting the rest.
  • +
+
+ """); + //#endif + + public SelectiveColumnsDemo() { + + // Pass explicit property names to the constructor to control which columns are created + // and in what initial order. Dot-notation addresses nested bean properties. + EasyGrid grid = new EasyGrid<>(Person.class, + "firstName", "lastName", "birthDate", "address.city", "address.postalCode"); + + // Further columns can be added individually or in bulk after construction. + grid.addColumn("age"); + grid.addColumns("phoneNumber", "subscriber"); + + // setColumnOrder reorders the listed columns and hides all others. + // phoneNumber and subscriber are not listed, so they are hidden. + grid.setColumnOrder("firstName", "lastName", "age", "birthDate", + "address.city", "address.postalCode"); + + // hideColumns hides specific columns without affecting the order of the rest. + grid.hideColumns("address.postalCode"); + + grid.setItems(service.fetchAll()); + add(grid); + add(description); //hide-source + setSizeFull(); + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/TypeRenderingDemo.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/TypeRenderingDemo.java new file mode 100644 index 0000000..d699403 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/TypeRenderingDemo.java @@ -0,0 +1,75 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid; + +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.flowingcode.vaadin.addons.easygrid.model.Person; +import com.flowingcode.vaadin.addons.easygrid.service.PersonService; +import com.vaadin.flow.component.Html; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import lombok.Getter; + +@DemoSource +@PageTitle("Type Rendering") +@SuppressWarnings("serial") +@Route(value = "easy-grid/type-rendering", layout = EasyGridDemoView.class) +@Getter // hide-source +public class TypeRenderingDemo extends Div { + + private final PersonService service = new PersonService(); + + //#if vaadin eq 0 + private final Html description = new Html(""" +
Demonstrates default type-driven rendering from GlobalEasyGridConfiguration + with no per-column configuration. +
    +
  • Number columns are right-aligned.
  • +
  • LocalDate is formatted as yyyy-MM-dd.
  • +
  • LocalDateTime is formatted as yyyy-MM-dd HH:mm:ss.
  • +
  • LocalTime is formatted as HH:mm:ss.
  • +
  • Null values are rendered as empty strings.
  • +
  • Boolean columns are center-aligned.
  • +
+
+ """); + //#endif + + public TypeRenderingDemo() { + + // No per-column configuration — all rendering is driven by GlobalEasyGridConfiguration defaults. + // + // age (int / Number) → right-aligned (Number.class → END alignment) + // birthDate (LocalDate) → "yyyy-MM-dd" (LocalDate global renderer factory) + // appointmentDateTime → "yyyy-MM-dd HH:mm:ss" (LocalDateTime global renderer factory) + // when null → "" (Object.class nullRepresentation = "") + // appointmentTime → "HH:mm:ss" (LocalTime global renderer factory, derived from appointmentDateTime) + // when null → "" (Object.class nullRepresentation = "") + // subscriber (boolean) → "true"/"false", CENTER (Boolean global formatter + alignment) + var grid = new EasyGrid<>(Person.class, + "firstName", "age", "birthDate", "appointmentDateTime", "appointmentTime", "subscriber"); + + grid.setItems(service.fetchAll()); + add(grid); + add(description); //hide-source + setSizeFull(); + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/TypedColumnDemo.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/TypedColumnDemo.java new file mode 100644 index 0000000..2431ffb --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/TypedColumnDemo.java @@ -0,0 +1,85 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid; + +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.flowingcode.vaadin.addons.easygrid.model.Person; +import com.flowingcode.vaadin.addons.easygrid.service.PersonService; +import com.vaadin.flow.component.Html; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import java.time.LocalDate; +import java.time.MonthDay; +import java.time.temporal.ChronoUnit; +import lombok.Getter; + +@DemoSource +@PageTitle("Typed Column") +@SuppressWarnings("serial") +@Route(value = "easy-grid/typed-column", layout = EasyGridDemoView.class) +@Getter // hide-source +public class TypedColumnDemo extends Div { + + private final PersonService service = new PersonService(); + + //#if vaadin eq 0 + private final Html description = new Html(""" +
Demonstrates addColumn(Class, ValueProvider) for computed values + with no corresponding bean property. +
    +
  • The explicit type drives type-aware configuration (renderers, formatters, alignment) automatically.
  • +
  • No key or header is derived automatically; configure them on the returned EasyColumn.
  • +
+
+ """); + //#endif + + public TypedColumnDemo() { + + EasyGrid grid = new EasyGrid<>(Person.class, false); + + // addColumn(Class, ValueProvider) adds a column for a computed value that has no + // corresponding bean property. The type drives the type-aware configuration + // (e.g. LocalDate columns get the configured date renderer automatically). + // No key or header is set automatically — configure them on the returned EasyColumn. + grid.addColumn(String.class, p -> p.getFirstName() + " " + p.getLastName()) + .setHeader("Full Name"); + + grid.addColumn("birthDate"); + + grid.addColumn(Integer.class, p -> { + LocalDate today = LocalDate.now(); + MonthDay birthMonthDay = MonthDay.from(p.getBirthDate()); + LocalDate nextBirthday = birthMonthDay.atYear(today.getYear()); + + if (nextBirthday.isBefore(today)) { + nextBirthday = birthMonthDay.atYear(today.getYear() + 1); + } + + return (int) ChronoUnit.DAYS.between(today, nextBirthday); + }).setHeader("Days Until Birthday"); + + grid.setItems(service.fetchAll()); + add(grid); + add(description); //hide-source + setSizeFull(); + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/data/PersonData.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/data/PersonData.java new file mode 100644 index 0000000..5f77089 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/data/PersonData.java @@ -0,0 +1,70 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.data; + +import com.flowingcode.vaadin.addons.easygrid.model.Address; +import com.flowingcode.vaadin.addons.easygrid.model.Person; +import com.github.javafaker.Faker; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Period; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; + +public class PersonData { + + private final List people = new ArrayList<>(); + + public List getPersons() { + if (people.isEmpty()) { + var faker = new Faker(); + for (int i = 0; i <= 10; i++) { + LocalDate minBirthDate = LocalDate.now().minusYears(99); + LocalDate maxBirthDate = LocalDate.now().minusYears(1); + int days = (int) ChronoUnit.DAYS.between(minBirthDate, maxBirthDate); + LocalDate birthDate = minBirthDate.plusDays(faker.number().numberBetween(0, days)); + int age = Period.between(birthDate, LocalDate.now()).getYears(); + + var person = Person.builder() + .id(i + 1) + .age(age) + .birthDate(birthDate) + .firstName(faker.name().firstName()) + .lastName(faker.name().lastName()) + .address(Address.builder() + .street(faker.address().streetAddress()) + .number(faker.random().nextInt(10000)) + .city(faker.address().city()) + .postalCode(faker.address().zipCode()).build()) + .phoneNumber(null) + .appointmentDateTime(faker.random().nextBoolean() + ? LocalDateTime.now().plusDays(faker.random().nextInt(90)) + : null) + .subscriber(faker.random().nextBoolean()) + .active(faker.random().nextBoolean()) + .build(); + + people.add(person); + } + } + return people; + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/it/AbstractViewTest.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/it/AbstractViewTest.java index ed4807d..51a1289 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/easygrid/it/AbstractViewTest.java +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/it/AbstractViewTest.java @@ -2,14 +2,14 @@ * #%L * Easy Grid Add-on * %% - * Copyright (C) 2023 Flowing Code + * Copyright (C) 2020 - 2026 Flowing Code * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/it/ViewIT.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/it/ViewIT.java deleted file mode 100644 index 2c71c21..0000000 --- a/src/test/java/com/flowingcode/vaadin/addons/easygrid/it/ViewIT.java +++ /dev/null @@ -1,64 +0,0 @@ -/*- - * #%L - * Easy Grid Add-on - * %% - * Copyright (C) 2023 Flowing Code - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -package com.flowingcode.vaadin.addons.easygrid.it; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.junit.Assert.assertThat; - -import com.vaadin.testbench.TestBenchElement; -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.hamcrest.TypeSafeDiagnosingMatcher; -import org.junit.Test; - -public class ViewIT extends AbstractViewTest { - - private Matcher hasBeenUpgradedToCustomElement = - new TypeSafeDiagnosingMatcher() { - - @Override - public void describeTo(Description description) { - description.appendText("a custom element"); - } - - @Override - protected boolean matchesSafely(TestBenchElement item, Description mismatchDescription) { - String script = "let s=arguments[0].shadowRoot; return !!(s&&s.childElementCount)"; - if (!item.getTagName().contains("-")) { - return true; - } - if ((Boolean) item.getCommandExecutor().executeScript(script, item)) { - return true; - } else { - mismatchDescription.appendText(item.getTagName() + " "); - mismatchDescription.appendDescriptionOf(is(not(this))); - return false; - } - } - }; - - @Test - public void componentWorks() { - TestBenchElement element = $("paper-input").first(); - assertThat(element, hasBeenUpgradedToCustomElement); - } -} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/model/Address.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/model/Address.java new file mode 100644 index 0000000..d0a6c71 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/model/Address.java @@ -0,0 +1,39 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.model; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +@Setter +public class Address { + + private String street; + private int number; + private String postalCode; + private String city; + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/model/NumberSample.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/model/NumberSample.java new file mode 100644 index 0000000..eedea9a --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/model/NumberSample.java @@ -0,0 +1,46 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.model; + +import java.math.BigDecimal; +import java.math.BigInteger; +import lombok.Getter; + +@Getter +public class NumberSample { + + private final BigDecimal bigDecimal; + private final BigInteger bigInteger; + private final Integer integer; + private final int intValue; + private final Long longValue; + private final double doubleValue; + + // Overflow is intentional: demo data exercises all numeric types including truncated values. + public NumberSample(BigDecimal value) { + this.bigDecimal = value; + this.bigInteger = value.toBigInteger(); + this.integer = bigInteger.intValue(); + this.intValue = integer; + this.longValue = bigInteger.longValue(); + this.doubleValue = value.doubleValue(); + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/model/Person.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/model/Person.java new file mode 100644 index 0000000..95bb106 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/model/Person.java @@ -0,0 +1,54 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.model; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +@Setter +@EqualsAndHashCode(of = "id") +public class Person { + + private int id; + private String firstName; + private String lastName; + private int age; + private Address address; + private String phoneNumber; + private LocalDate birthDate; + private LocalDateTime appointmentDateTime; + private boolean subscriber; + private boolean active; + + public LocalTime getAppointmentTime() { + return appointmentDateTime != null ? appointmentDateTime.toLocalTime() : null; + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/service/PersonService.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/service/PersonService.java new file mode 100644 index 0000000..6aa8f59 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/service/PersonService.java @@ -0,0 +1,42 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.service; + +import com.flowingcode.vaadin.addons.easygrid.data.PersonData; +import com.flowingcode.vaadin.addons.easygrid.model.Person; +import java.util.List; + +public class PersonService { + + private final PersonData personData = new PersonData(); + + public List fetch(int offset, int limit) { + return personData.getPersons().stream().skip(offset).limit(limit).toList(); + } + + public int count() { + return personData.getPersons().size(); + } + + public List fetchAll() { + return personData.getPersons(); + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/ColumnConfigurationImplParentDelegationTest.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/ColumnConfigurationImplParentDelegationTest.java new file mode 100644 index 0000000..b7bf152 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/ColumnConfigurationImplParentDelegationTest.java @@ -0,0 +1,81 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.test; + +import static org.junit.Assert.assertNotSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import com.flowingcode.vaadin.addons.easygrid.config.ColumnConfiguration; +import java.lang.reflect.Method; +import java.util.Collection; +import org.junit.runners.Parameterized.Parameters; +import org.mockito.Mockito; + +/** + * Verifies that every getter on {@link ColumnConfigurationImpl} delegates to the parent when its + * local field is {@code null}, and that no non-getter method (setters, etc.) calls into the parent + * at all. + * + *

Methods are discovered reflectively from {@link ColumnConfiguration}. Getters (names starting + * with {@code "get"}) are verified to call through to the parent; all other public methods are + * verified to call only getter methods on the parent (never setters). + */ +public class ColumnConfigurationImplParentDelegationTest extends DelegationTest { + + public ColumnConfigurationImplParentDelegationTest(String name, Method method) { + super(method); + } + + @Parameters(name = "{0}") + public static Collection parameters() { + return parameters(ColumnConfiguration.class, m -> true); + } + + @Override + protected ColumnConfiguration createDelegate() throws ClassNotFoundException { + return (ColumnConfiguration) mock(EasyColumnTestHelper.columnConfigurationImplClass()); + } + + @Override + protected ColumnConfiguration createTarget(Object delegate) + throws ReflectiveOperationException { + return EasyColumnTestHelper.newColumnConfigurationImpl((ColumnConfiguration) delegate); + } + + @Override + protected void assertDelegated(Object parent, String methodName, Object[] args) + throws ReflectiveOperationException { + if (method.getName().startsWith("get")) { + method.invoke(Mockito.verify(parent)); + } else { + verifyNoInteractions(parent); + } + } + + @Override + protected void verifyResult(Object target, Object result, String methodName) { + if (methodName.equals("createNewLayer")) { + assertNotSame(methodName + " must return a new instance", target, result); + } else { + super.verifyResult(target, result, methodName); + } + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/ColumnConfigurationLinkDelegationTest.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/ColumnConfigurationLinkDelegationTest.java new file mode 100644 index 0000000..ff78f61 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/ColumnConfigurationLinkDelegationTest.java @@ -0,0 +1,126 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.test; + +import static org.junit.Assert.assertNotSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import com.flowingcode.vaadin.addons.easygrid.config.ColumnConfiguration; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.junit.runners.Parameterized.Parameters; +import org.mockito.Mockito; + +/** + * Verifies that every getter on {@link ColumnConfigurationLink} checks the primary config first + * and falls back to the fallback config, and that no non-getter method calls into the fallback. + * + *

Methods are discovered reflectively from {@link ColumnConfiguration}. Each getter is + * exercised twice: once with the primary unstubbed (returns {@code null}), in which case the + * link must fall back to the fallback config; and once with the primary stubbed to return a + * non-{@code null} value, in which case the fallback must not be called. For non-getter methods, + * no interaction with the fallback is expected. + */ +public class ColumnConfigurationLinkDelegationTest extends DelegationTest { + + private final boolean stubbed; + private ColumnConfiguration fallbackMock; + + public ColumnConfigurationLinkDelegationTest(String name, Method method, boolean stubbed) { + super(method); + this.stubbed = stubbed; + } + + @Parameters(name = "{0}") + public static Collection parameters() { + List result = new ArrayList<>(); + for (Object[] row : parameters(ColumnConfiguration.class, m -> true)) { + Method m = (Method) row[1]; + if (m.getName().startsWith("get")) { + result.add(new Object[] {row[0], m, false}); + result.add(new Object[] {row[0] + " [primary stubbed]", m, true}); + } else { + result.add(new Object[] {row[0], m, false}); + } + } + return result; + } + + @Override + protected ColumnConfiguration createDelegate() throws ClassNotFoundException { + return (ColumnConfiguration) mock(EasyColumnTestHelper.columnConfigurationImplClass()); + } + + @Override + protected ColumnConfiguration createTarget(Object primary) + throws ReflectiveOperationException { + fallbackMock = + (ColumnConfiguration) mock(EasyColumnTestHelper.columnConfigurationImplClass()); + if (stubbed) { + stubGetter(method, (ColumnConfiguration) primary); + } + return EasyColumnTestHelper.newColumnConfigurationLink((ColumnConfiguration) primary, + fallbackMock); + } + + @Override + protected void assertDelegated(Object primary, String methodName, Object[] args) + throws ReflectiveOperationException { + if (method.getName().startsWith("get") && !stubbed) { + // primary returned null, so the link fell back to fallbackMock + method.invoke(Mockito.verify(fallbackMock)); + } else { + // primary was stubbed non-null (getter) or setter went to primary — fallbackMock untouched + verifyNoInteractions(fallbackMock); + } + } + + /** + * Stubs {@code mock.getter()} to return a non-{@code null} value so that + * {@link ColumnConfigurationLink}'s getter short-circuits without reaching the fallback. + */ + @SuppressWarnings("rawtypes") + private void stubGetter(Method getter, ColumnConfiguration mock) + throws ReflectiveOperationException { + Class returnType = getter.getReturnType(); + Object value; + if (returnType == String.class) { + value = ""; + } else if (returnType.isEnum()) { + value = returnType.getEnumConstants()[0]; + } else { + value = Mockito.mock(returnType); + } + ColumnConfiguration proxy = Mockito.doReturn(value).when(mock); + getter.invoke(proxy); + } + + @Override + protected void verifyResult(Object target, Object result, String methodName) { + if (methodName.equals("createNewLayer")) { + assertNotSame(methodName + " must return a new instance", target, result); + } else { + super.verifyResult(target, result, methodName); + } + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/DelegationTest.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/DelegationTest.java new file mode 100644 index 0000000..30e6870 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/DelegationTest.java @@ -0,0 +1,202 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.test; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.Mockito; + +/** + * Generic base for parameterized delegation tests. + * + *

Verifies that for each method supplied by the subclass's {@code @Parameters}, the target + * class (a) exposes the method, (b) returns {@code this} (chainability), and (c) produces the + * expected side-effect on the underlying delegate. If the target class does not expose the method + * at all, the test fails with a descriptive message. + * + *

Concrete subclasses must supply: + *

    + *
  • a {@code @Parameters} static method that calls + * {@link #parameters(Class) parameters(delegateClass)};
  • + *
  • {@link #filter(Method)}: selects which methods are exercised;
  • + *
  • implementations of {@link #getTargetClass()}, {@link #getDelegateClass()}, + * {@link #createDelegate()}, {@link #createTarget(Object)}, + * {@link #buildArg(Class, int)}, and {@link #assertDelegated(Object, String, Object[])}.
  • + *
+ */ +@RunWith(Parameterized.class) +public abstract class DelegationTest { + + protected final Method method; + + protected DelegationTest(Method method) { + this.method = method; + method.setAccessible(true); + } + + /** Returns the class under test, derived from the return type of {@link #createTarget}. */ + protected final Class getTargetClass() { + for (Class c = getClass(); c != DelegationTest.class; c = c.getSuperclass()) { + try { + return c.getDeclaredMethod("createTarget", Object.class).getReturnType(); + } catch (NoSuchMethodException ignored) { + } + } + return Object.class; + } + + /** + * Builds the {@code @Parameters} collection from all public methods of {@code delegateClass}. + * + *

Each element is {@code {methodName, method}}. Methods excluded by {@link #filter} are + * skipped at runtime rather than at parameterization time. + * + * @param delegateClass the delegate class whose methods are enumerated + * @return parameter rows for {@link Parameterized} + */ + protected static Collection parameters(Class delegateClass, + Predicate filter) { + return Arrays.stream(delegateClass.getMethods()) + .filter(m -> Modifier.isPublic(m.getModifiers())) + .filter(m -> !Modifier.isStatic(m.getModifiers())) + .filter(m -> m.getDeclaringClass() != Object.class) + .filter(filter) + .sorted(Comparator.comparing(Method::getName).thenComparingInt(Method::getParameterCount)) + .map(m -> new Object[] {signatureOf(m), m}) + .collect(Collectors.toList()); + } + + private static Object signatureOf(Method m) { + return m.getName() + Stream.of(m.getParameterTypes()).map(Class::getSimpleName) + .collect(Collectors.joining(",", "(", ")")); + } + + /** + * Creates a fresh delegate instance for each test run. Returning {@code null} skips target + * construction and only verifies that the target class exposes the method signature. + */ + protected abstract Object createDelegate() throws ReflectiveOperationException; + + /** + * Creates a fresh target instance wrapping {@code delegate}. Only called when + * {@link #createDelegate()} returns a non-null value. + */ + protected abstract Object createTarget(Object delegate) throws ReflectiveOperationException; + + /** + * Returns a test argument for a parameter of the given type at position {@code index}. + * + * @throws UnsupportedOperationException if no factory is registered for {@code paramType} + */ + private final Object buildArg(Class type) { + if (type == String.class) { + return "value"; + } else if (type.isEnum()) { + return type.getEnumConstants()[0]; + } else if (type == boolean.class) { + return Boolean.TRUE; + } else if (type == int.class) { + return Integer.valueOf(0); + } else if (type.isArray()) { + return Array.newInstance(type.getComponentType(), 0); + } else { + return Mockito.mock(type); + } + } + + /** + * Asserts that invoking {@code methodName(args)} on the target has produced the expected + * side-effect on {@code delegate}. + */ + protected abstract void assertDelegated(Object delegate, String methodName, Object[] args) + throws ReflectiveOperationException; + + @Test + public final void testMethod() throws Throwable { + Object delegate = createDelegate(); + + Method targetMethod; + try { + targetMethod = getTargetClass().getMethod(method.getName(), method.getParameterTypes()); + } catch (NoSuchMethodException e) { + fail(getTargetClass().getSimpleName() + " does not expose " + signatureOf(method)); + return; + } + + verifyMethod(targetMethod); + + if (delegate != null) { + Object target = createTarget(delegate); + if (target == null) { + fail("createTarget() must not return null when createDelegate() returns non-null"); + return; + } + + try { + Object[] args = buildArgs(method.getParameterTypes()); + targetMethod.setAccessible(true); + Object result = targetMethod.invoke(target, args); + if (isChainable(targetMethod)) { + verifyResult(target, result, method.getName()); + } + assertDelegated(delegate, method.getName(), args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + } + + protected void verifyMethod(Method targetMethod) { + // Do nothing + } + + /** + * Returns {@code true} if the method under test is expected to return the target instance + */ + protected boolean isChainable(Method targetMethod) { + return !method.getName().startsWith("get"); + } + + protected void verifyResult(Object target, Object result, String methodName) { + assertSame(methodName + " must return target", target, result); + } + + private Object[] buildArgs(Class[] types) { + Object[] args = new Object[types.length]; + for (int i = 0; i < types.length; i++) { + args[i] = buildArg(types[i]); + } + return args; + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/EasyColumnTestHelper.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/EasyColumnTestHelper.java new file mode 100644 index 0000000..4b00cbf --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/EasyColumnTestHelper.java @@ -0,0 +1,77 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.test; + +import static org.mockito.Mockito.mock; +import com.flowingcode.vaadin.addons.easygrid.EasyColumn; +import com.flowingcode.vaadin.addons.easygrid.config.ColumnConfiguration; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.function.ValueProvider; +import java.lang.reflect.Constructor; +import org.mockito.Mockito; + +final class EasyColumnTestHelper { + + private EasyColumnTestHelper() {} + + static Class columnConfigurationImplClass() throws ClassNotFoundException { + return Class.forName("com.flowingcode.vaadin.addons.easygrid.config.ColumnConfigurationImpl"); + } + + static Class columnConfigurationLinkClass() throws ClassNotFoundException { + return Class.forName("com.flowingcode.vaadin.addons.easygrid.config.ColumnConfigurationLink"); + } + + @SuppressWarnings("unchecked") + static ColumnConfiguration mockColumnConfigurationImpl() throws ClassNotFoundException { + var mock = (ColumnConfiguration) mock(columnConfigurationImplClass()); + Mockito.when(mock.getRendererFactory()) + .thenReturn(getter -> new com.vaadin.flow.data.renderer.TextRenderer<>(Object::toString)); + return mock; + } + + @SuppressWarnings("unchecked") + static ColumnConfiguration newColumnConfigurationImpl(ColumnConfiguration parent) + throws ReflectiveOperationException { + var ctor = columnConfigurationImplClass().getDeclaredConstructor(ColumnConfiguration.class); + ctor.setAccessible(true); + return (ColumnConfiguration) ctor.newInstance(parent); + } + + @SuppressWarnings("unchecked") + static ColumnConfiguration newColumnConfigurationLink(ColumnConfiguration primary, + ColumnConfiguration fallback) throws ReflectiveOperationException { + var ctor = columnConfigurationLinkClass().getDeclaredConstructor(ColumnConfiguration.class, + ColumnConfiguration.class); + ctor.setAccessible(true); + return (ColumnConfiguration) ctor.newInstance(primary, fallback); + } + + @SuppressWarnings("rawtypes") + static EasyColumn newEasyColumn(ColumnConfiguration config, Grid.Column column) + throws ReflectiveOperationException { + Constructor ctor = EasyColumn.class.getDeclaredConstructor( + ColumnConfiguration.class, Grid.Column.class, ValueProvider.class, Class.class); + ctor.setAccessible(true); + ValueProvider getter = x -> x; + return ctor.newInstance(config, column, getter, Object.class); + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/EasyColumnToConfigurationDelegationTest.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/EasyColumnToConfigurationDelegationTest.java new file mode 100644 index 0000000..df4cb64 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/EasyColumnToConfigurationDelegationTest.java @@ -0,0 +1,103 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.test; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import com.flowingcode.vaadin.addons.easygrid.EasyColumn; +import com.flowingcode.vaadin.addons.easygrid.config.ColumnConfiguration; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.data.renderer.Renderer; +import com.vaadin.flow.function.SerializableBiFunction; +import java.lang.reflect.Method; +import java.util.Collection; +import org.junit.runners.Parameterized.Parameters; + +/** + * Verifies that every chainable setter on {@link ColumnConfiguration} is exposed and delegated by + * {@link EasyColumn}. + * + *

Methods are discovered reflectively from {@link ColumnConfiguration}: every public method + * whose name starts with {@code "set"} and whose return type is {@code IColumnConfiguration} is + * tested. If {@link EasyColumn} does not expose a corresponding method, the test fails with a + * clear message, making coverage gaps immediately visible. + * + *

Delegation is verified via Mockito: the delegate is a mock, and after invoking the + * corresponding {@link EasyColumn} setter, {@link org.mockito.Mockito#verify verify} asserts that + * the setter was called on the mock with the expected arguments. + * + * @see DelegationTest + */ +public class EasyColumnToConfigurationDelegationTest extends DelegationTest { + + private Grid.Column columnMock; + + public EasyColumnToConfigurationDelegationTest(String name, Method setter) { + super(setter); + } + + @Parameters(name = "{0}") + public static Collection parameters() { + return parameters(ColumnConfiguration.class, m -> { + switch (m.getName()) { + case "setRendererFactory": + return false; + case "setFormatter": + return m.getParameterTypes()[0] != SerializableBiFunction.class; + default: + return m.getName().startsWith("set") + && m.getReturnType() == ColumnConfiguration.class; + } + }); + } + + @Override + protected ColumnConfiguration createDelegate() throws ClassNotFoundException { + return EasyColumnTestHelper.mockColumnConfigurationImpl(); + } + + @Override + protected EasyColumn createTarget(Object delegate) throws ReflectiveOperationException { + columnMock = mock(Grid.Column.class); + return EasyColumnTestHelper.newEasyColumn((ColumnConfiguration) delegate, columnMock); + } + + @Override + @SuppressWarnings("unchecked") + protected void assertDelegated(Object delegate, String methodName, Object[] args) + throws ReflectiveOperationException { + method.invoke(verify(delegate), args); + if (updatesRenderer(methodName)) { + verify(columnMock).setRenderer(any(Renderer.class)); + } + } + + private static boolean updatesRenderer(String methodName) { + switch (methodName) { + case "setNullRepresentation": + case "setFormatter": + return true; + default: + return false; + } + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/EasyColumnToGridColumnDelegationTest.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/EasyColumnToGridColumnDelegationTest.java new file mode 100644 index 0000000..4d24243 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/EasyColumnToGridColumnDelegationTest.java @@ -0,0 +1,90 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import com.flowingcode.vaadin.addons.easygrid.EasyColumn; +import com.flowingcode.vaadin.addons.easygrid.config.ColumnConfiguration; +import com.vaadin.flow.component.grid.Grid; +import java.lang.reflect.Method; +import java.util.Collection; +import org.junit.runners.Parameterized.Parameters; + +/** + * Verifies that every chainable setter on {@link Grid.Column} is exposed and delegated by + * {@link EasyColumn}. + * + *

Methods are discovered reflectively from {@link Grid.Column}: every public method whose name + * starts with {@code "set"} and is declared on {@code Grid.Column} or an interface is tested. If + * {@link EasyColumn} does not expose a corresponding method, the test fails with a clear message, + * making coverage gaps immediately visible. + * + *

Delegation is verified via Mockito: the delegate is a mock {@link Grid.Column}, and after + * invoking the corresponding {@link EasyColumn} setter, {@link org.mockito.Mockito#verify verify} + * asserts that the setter was called on the mock with the expected arguments. + * + * @see DelegationTest + */ +public class EasyColumnToGridColumnDelegationTest extends DelegationTest { + + public EasyColumnToGridColumnDelegationTest(String name, Method setter) { + super(setter); + } + + @Parameters(name = "{0}") + public static Collection parameters() { + return parameters(Grid.Column.class, m -> { + switch (m.getName()) { + case "setClassName": + case "setClassNameGenerator": + case "setRenderer": + return false; + default: + return (m.getDeclaringClass() == Grid.Column.class + || m.getDeclaringClass().isInterface()) && m.getName().startsWith("set"); + } + }); + } + + @Override + protected Grid.Column createDelegate() throws ReflectiveOperationException { + return mock(Grid.Column.class); + } + + @Override + protected EasyColumn createTarget(Object delegate) throws ReflectiveOperationException { + return EasyColumnTestHelper.newEasyColumn( + EasyColumnTestHelper.mockColumnConfigurationImpl(), (Grid.Column) delegate); + } + + @Override + protected void verifyMethod(Method targetMethod) { + assertEquals(EasyColumn.class, targetMethod.getReturnType()); + } + + @Override + protected void assertDelegated(Object delegate, String methodName, Object[] args) + throws ReflectiveOperationException { + method.invoke(verify(delegate), args); + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/EasyGridConstructionTest.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/EasyGridConstructionTest.java new file mode 100644 index 0000000..ac68759 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/EasyGridConstructionTest.java @@ -0,0 +1,98 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import com.flowingcode.vaadin.addons.easygrid.EasyColumn; +import com.flowingcode.vaadin.addons.easygrid.EasyGrid; +import com.flowingcode.vaadin.addons.easygrid.model.Person; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.grid.Grid; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; +import org.junit.After; +import org.junit.Test; +import org.mockito.Mockito; + +public class EasyGridConstructionTest { + + @After + public void tearDownUI() { + UI.setCurrent(null); + } + + @Test + public void autoDiscoveryCreatesColumnsForBeanProperties() { + UI ui = Mockito.mock(UI.class); + Mockito.when(ui.getLocale()).thenReturn(Locale.ENGLISH); + UI.setCurrent(ui); + assertNotNull("UI must be set by @Before before EasyGrid construction", UI.getCurrent()); + EasyGrid grid = new EasyGrid<>(Person.class); + List keys = grid.getWrappedGrid().getColumns().stream() + .map(Grid.Column::getKey) + .collect(Collectors.toList()); + assertThat(keys, hasItems("firstName", "lastName", "age", "birthDate", "subscriber", "active")); + } + + @Test + public void explicitPropertyConstructorCreatesOnlyListedColumns() { + EasyGrid grid = new EasyGrid<>(Person.class, "firstName", "lastName"); + List keys = grid.getWrappedGrid().getColumns().stream() + .map(Grid.Column::getKey) + .collect(Collectors.toList()); + assertThat(keys, hasSize(2)); + assertThat(keys, hasItems("firstName", "lastName")); + } + + @Test + public void addColumnWithTypeAndGetterHasNoKey() { + EasyGrid grid = new EasyGrid<>(Person.class, false); + EasyColumn col = + grid.addColumn(String.class, p -> p.getFirstName() + " " + p.getLastName()); + assertNull(col.getColumn().getKey()); + assertThat(grid.getWrappedGrid().getColumns(), hasSize(1)); + } + + @Test + public void setColumnOrderShowsOnlyListedColumns() { + EasyGrid grid = new EasyGrid<>(Person.class, "firstName", "lastName", "address"); + grid.setColumnOrder("lastName", "firstName"); + assertTrue(grid.getWrappedGrid().getColumnByKey("lastName").isVisible()); + assertTrue(grid.getWrappedGrid().getColumnByKey("firstName").isVisible()); + assertFalse(grid.getWrappedGrid().getColumnByKey("address").isVisible()); + } + + @Test + public void hideColumnsHidesSpecifiedColumns() { + EasyGrid grid = new EasyGrid<>(Person.class, "firstName", "lastName", "address"); + grid.hideColumns("address", "firstName"); + assertFalse(grid.getWrappedGrid().getColumnByKey("address").isVisible()); + assertFalse(grid.getWrappedGrid().getColumnByKey("firstName").isVisible()); + assertTrue(grid.getWrappedGrid().getColumnByKey("lastName").isVisible()); + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/GlobalEasyGridConfigurationFreezeTest.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/GlobalEasyGridConfigurationFreezeTest.java new file mode 100644 index 0000000..2869486 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/GlobalEasyGridConfigurationFreezeTest.java @@ -0,0 +1,93 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import com.flowingcode.vaadin.addons.easygrid.config.ColumnConfiguration; +import com.flowingcode.vaadin.addons.easygrid.config.GlobalEasyGridConfiguration; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.time.LocalDate; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** Verifies {@link GlobalEasyGridConfiguration#freeze()} behaviour. */ +public class GlobalEasyGridConfigurationFreezeTest { + + private static void setFrozen(boolean value) throws ReflectiveOperationException { + Field field = GlobalEasyGridConfiguration.class.getDeclaredField("frozen"); + field.setAccessible(true); + field.set(null, value); + } + + @SuppressWarnings("unchecked") + private static ColumnConfiguration resolve(Class type) + throws ReflectiveOperationException { + Method method = GlobalEasyGridConfiguration.class.getDeclaredMethod("resolve", Class.class); + method.setAccessible(true); + return (ColumnConfiguration) method.invoke(null, type); + } + + @Before + public void ensureUnfrozen() throws ReflectiveOperationException { + setFrozen(false); + } + + @After + public void restoreUnfrozen() throws ReflectiveOperationException { + setFrozen(false); + } + + @Test + public void resolveBeforeFreezeCreatesEntry() throws ReflectiveOperationException { + assertNotNull(resolve(LocalDate.class)); + } + + @Test + public void afterFreezeIsFrozen() { + GlobalEasyGridConfiguration.freeze(); + assertTrue(GlobalEasyGridConfiguration.isFrozen()); + } + + @Test(expected = IllegalStateException.class) + public void forTypeAfterFreezeThrows() { + GlobalEasyGridConfiguration.freeze(); + GlobalEasyGridConfiguration.forType(String.class); + } + + @Test + public void resolveAfterFreezeReturnsRegisteredConfig() throws ReflectiveOperationException { + GlobalEasyGridConfiguration.freeze(); + // LocalDate was registered in the static initializer + assertNotNull(resolve(LocalDate.class)); + } + + @Test + public void resolveAfterFreezeReturnsNullForUnregisteredType() + throws ReflectiveOperationException { + GlobalEasyGridConfiguration.freeze(); + // No config registered for this marker interface + assertNull(resolve(Runnable.class)); + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/GlobalEasyGridConfigurationTest.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/GlobalEasyGridConfigurationTest.java new file mode 100644 index 0000000..519ccde --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/GlobalEasyGridConfigurationTest.java @@ -0,0 +1,57 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import com.flowingcode.vaadin.addons.easygrid.config.ColumnConfiguration; +import com.flowingcode.vaadin.addons.easygrid.config.GlobalEasyGridConfiguration; +import com.vaadin.flow.component.grid.ColumnTextAlign; +import java.lang.reflect.Method; +import org.junit.Test; + +public class GlobalEasyGridConfigurationTest { + + @SuppressWarnings("unchecked") + private static ColumnConfiguration resolve(Class type) + throws ReflectiveOperationException { + Method method = GlobalEasyGridConfiguration.class.getDeclaredMethod("resolve", Class.class); + method.setAccessible(true); + return (ColumnConfiguration) method.invoke(null, type); + } + + @Test + public void getOrCreateWithIntShouldNotThrow() throws ReflectiveOperationException { + assertNotNull(resolve(int.class)); + } + + @Test + public void getOrCreateWithBooleanShouldNotThrow() throws ReflectiveOperationException { + assertNotNull(resolve(boolean.class)); + } + + @Test + public void booleanPrimitiveParentIsBoolean() throws ReflectiveOperationException { + // Boolean.class has textAlign=CENTER set in GlobalEasyGridConfiguration static initializer; + // boolean.class must inherit it, which only happens if Boolean.class is its parent. + assertSame(ColumnTextAlign.CENTER, resolve(boolean.class).getTextAlign()); + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/GlobalRendererFactoryTest.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/GlobalRendererFactoryTest.java new file mode 100644 index 0000000..d8025f9 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/GlobalRendererFactoryTest.java @@ -0,0 +1,59 @@ +/*- + * #%L + * Easy Grid Add-on + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.easygrid.test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import com.flowingcode.vaadin.addons.easygrid.config.InstanceEasyGridConfiguration; +import com.vaadin.flow.data.renderer.LocalDateRenderer; +import com.vaadin.flow.data.renderer.LocalDateTimeRenderer; +import com.vaadin.flow.data.renderer.TextRenderer; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import org.junit.Test; + +public class GlobalRendererFactoryTest { + + @Test + public void globalLocalDateRendererFactoryIsApplied() { + // LocalDateRenderer factory is applied from global default + var config = new InstanceEasyGridConfiguration().resolve(LocalDate.class); + var renderer = config.getRendererFactory().apply(v -> (LocalDate) v); + assertThat(renderer, instanceOf(LocalDateRenderer.class)); + } + + @Test + public void globalLocalDateTimeRendererFactoryIsApplied() { + // LocalDateTimeRenderer factory is applied from global default + var config = new InstanceEasyGridConfiguration().resolve(LocalDateTime.class); + var renderer = config.getRendererFactory().apply(v -> (LocalDateTime) v); + assertThat(renderer, instanceOf(LocalDateTimeRenderer.class)); + } + + @Test + public void globalLocalTimeRendererFactoryIsApplied() { + // TextRenderer factory is applied from global default for LocalTime + var config = new InstanceEasyGridConfiguration().resolve(LocalTime.class); + var renderer = config.getRendererFactory().apply(v -> (LocalTime) v); + assertThat(renderer, instanceOf(TextRenderer.class)); + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/SerializationTest.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/SerializationTest.java index b609b50..421d023 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/SerializationTest.java +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/test/SerializationTest.java @@ -2,14 +2,14 @@ * #%L * Easy Grid Add-on * %% - * Copyright (C) 2023 Flowing Code + * Copyright (C) 2020 - 2026 Flowing Code * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,14 +19,17 @@ */ package com.flowingcode.vaadin.addons.easygrid.test; -import com.flowingcode.vaadin.addons.easygrid.EasyGridAddon; +import com.flowingcode.vaadin.addons.easygrid.EasyGrid; +import com.flowingcode.vaadin.addons.easygrid.model.Person; +import com.vaadin.flow.component.UI; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; -import org.junit.Assert; +import java.util.Locale; import org.junit.Test; +import org.mockito.Mockito; public class SerializationTest { @@ -42,11 +45,19 @@ private void testSerializationOf(Object obj) throws IOException, ClassNotFoundEx } @Test - public void testSerialization() throws ClassNotFoundException, IOException { + public void testSerialization() throws Exception { + EasyGrid easyGrid; try { - testSerializationOf(new EasyGridAddon()); - } catch (Exception e) { - Assert.fail("Problem while testing serialization: " + e.getMessage()); + UI ui = Mockito.mock(UI.class); + Mockito.when(ui.getLocale()).thenReturn(Locale.ENGLISH); + UI.setCurrent(ui); + easyGrid = new EasyGrid<>(Person.class, + "firstName", "lastName", "birthDate", "age", "subscriber"); + } finally { + UI.setCurrent(null); } + + testSerializationOf(easyGrid); } + } diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/EasyGridAddon.java b/src/test/resources/META-INF/frontend/styles/easy-grid-demo-styles.css similarity index 56% rename from src/main/java/com/flowingcode/vaadin/addons/easygrid/EasyGridAddon.java rename to src/test/resources/META-INF/frontend/styles/easy-grid-demo-styles.css index 7933066..cba4e6f 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/easygrid/EasyGridAddon.java +++ b/src/test/resources/META-INF/frontend/styles/easy-grid-demo-styles.css @@ -2,7 +2,7 @@ * #%L * Easy Grid Add-on * %% - * Copyright (C) 2023 Flowing Code + * Copyright (C) 2020 - 2026 Flowing Code * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,16 +17,6 @@ * limitations under the License. * #L% */ - -package com.flowingcode.vaadin.addons.easygrid; - -import com.vaadin.flow.component.Tag; -import com.vaadin.flow.component.dependency.JsModule; -import com.vaadin.flow.component.dependency.NpmPackage; -import com.vaadin.flow.component.html.Div; - -@SuppressWarnings("serial") -@NpmPackage(value = "@polymer/paper-input", version = "3.2.1") -@JsModule("@polymer/paper-input/paper-input.js") -@Tag("paper-input") -public class EasyGridAddon extends Div {} +.easy-grid vaadin-grid { + margin-bottom: var(--lumo-space-m, 1rem); +} diff --git a/src/test/resources/META-INF/frontend/styles/shared-styles.css b/src/test/resources/META-INF/frontend/styles/shared-styles.css deleted file mode 100644 index 6680e2d..0000000 --- a/src/test/resources/META-INF/frontend/styles/shared-styles.css +++ /dev/null @@ -1 +0,0 @@ -/*Demo styles*/ \ No newline at end of file