Skip to content

Commit d9ddec2

Browse files
committed
docs: update specifications
1 parent f30af8c commit d9ddec2

3 files changed

Lines changed: 345 additions & 236 deletions

File tree

CONFIGURATION_RESOLUTION.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Configuration Resolution: Scope-First vs. Type-First
2+
3+
## The Two Strategies
4+
5+
Given `Foo extends Entity` with configurations at four points in the matrix:
6+
7+
| | Instance | Global |
8+
|---|---|---|
9+
| **Foo** | I·Foo | G·Foo |
10+
| **Entity** | I·Entity | G·Entity |
11+
12+
**Scope-first** exhausts all instance settings before looking at global ones:
13+
```
14+
I·Foo → I·Entity → G·Foo → G·Entity → Default
15+
```
16+
17+
**Type-first** exhausts all Foo-specific settings before looking at Entity:
18+
```
19+
I·Foo → G·Foo → I·Entity → G·Entity → Default
20+
```
21+
22+
Both agree on `I·Foo` being first and `G·Entity` being last. The dispute is the middle two.
23+
24+
---
25+
26+
## Where Scope-First Makes Sense
27+
28+
**Setting: `textAlign` or `nullRepresentation`**
29+
30+
A developer creates a reporting grid and writes:
31+
```java
32+
easyGrid.configuration.forType(Entity.class).setTextAlign(RIGHT);
33+
```
34+
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.
35+
36+
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.
37+
38+
---
39+
40+
## Where Type-First Makes Sense
41+
42+
**Setting: `rendererFactory`**
43+
44+
Globally, `Foo` has a specialized renderer registered:
45+
```java
46+
GlobalEasyGridConfiguration.forType(Foo.class)
47+
.setRendererFactory(FooRenderers.of(...));
48+
```
49+
Independently, an instance configures Entity columns with a generic formatter:
50+
```java
51+
easyGrid.configuration.forType(Entity.class).setFormatter(e -> e.getId().toString());
52+
```
53+
54+
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.
55+
56+
Type-first preserves the type contract: `G·Foo` sits above `I·Entity`, so Foo's rendering semantics are honoured even across scope boundaries.
57+
58+
---
59+
60+
## Liskov Substitution Principle
61+
62+
LSP says: a `Foo` must be usable wherever an `Entity` is expected, without callers needing to know the difference.
63+
64+
Applied to configuration, it has two implications that pull in opposite directions:
65+
66+
**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.
67+
68+
**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.
69+
70+
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.**
71+
72+
---
73+
74+
## Current Implementation
75+
76+
The implementation uses **scope-first** for all properties. For a `Foo extends Entity` column the effective chain is:
77+
78+
```
79+
per-column → I·Foo → I·Entity → I·Object → G·Foo → G·Entity → G·Object → Default
80+
```
81+
82+
This is built in two pieces:
83+
84+
**Instance chain**`EasyGridConfigurationClassMap.getOrCreate(Foo)` walks the Java class hierarchy within the same map, producing a `ColumnConfigurationImpl` chain:
85+
86+
```
87+
I·Foo(impl) → I·Entity(impl) → I·Object(impl)
88+
```
89+
90+
**Global chain**`GlobalEasyGridConfiguration.resolve(Foo)` similarly produces:
91+
92+
```
93+
G·Foo(impl) → G·Entity(impl) → G·Object(impl)
94+
```
95+
96+
**Bridge**`InstanceEasyGridConfiguration.forType(Foo)` wraps both into a `ColumnConfigurationLink`:
97+
98+
```
99+
ColumnConfigurationLink(primary = I·Foo chain, fallback = G·Foo chain)
100+
```
101+
102+
`ColumnConfigurationLink.get()` consults the primary chain first and only falls back to the global chain when the primary returns `null` for a given property.
103+
104+
**Per-column**`InstanceEasyGridConfiguration.resolve(Foo)` wraps the link in a fresh `ColumnConfigurationImpl` whose fields are overridden by column-level setters (`setNullRepresentation`, `setFormatter`, `setRendererFactory`).
105+
106+
**Default** — when the entire chain returns `null` for `getRendererFactory()`, `EasyColumn.createRenderer` applies a `ColumnConfigurationTextRenderer` with null-representation support as the last resort.

FEATURE_ROW_ACTIONS.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Feature: Row Actions
2+
3+
Row actions are buttons or menu items displayed in a dedicated actions column, created and managed by `EasyGrid` on the wrapped grid.
4+
5+
## API
6+
7+
### `EasyGrid<T>` methods
8+
9+
```java
10+
// Add an action button (label + icon)
11+
EasyRowAction<T> addRowAction(String label, VaadinIcon icon, SerializableConsumer<T> handler);
12+
13+
// Add an action button with a theme variant
14+
EasyRowAction<T> addRowAction(String label, VaadinIcon icon, ButtonVariant variant, SerializableConsumer<T> handler);
15+
16+
// Render all actions as a context menu (overflow menu) instead of inline buttons
17+
void setRowActionsAsMenu(boolean asMenu);
18+
19+
// Access the underlying Grid.Column for header, width, freezing, etc.
20+
Grid.Column<T> getActionsColumn();
21+
```
22+
23+
### `EasyRowAction<T>`
24+
25+
```java
26+
public class EasyRowAction<T> {
27+
// Conditional visibility
28+
EasyRowAction<T> withVisibleWhen(SerializablePredicate<T> predicate);
29+
30+
// Conditional enablement
31+
EasyRowAction<T> withEnabledWhen(SerializablePredicate<T> predicate);
32+
33+
// Tooltip
34+
EasyRowAction<T> withTooltip(String tooltip);
35+
EasyRowAction<T> withTooltip(SerializableFunction<T, String> tooltipProvider);
36+
37+
// Confirmation dialog before executing the action
38+
EasyRowAction<T> withConfirmation(String message);
39+
EasyRowAction<T> withConfirmation(String title, String message);
40+
}
41+
```
42+
43+
## Usage
44+
45+
```java
46+
// Inline action buttons
47+
easyGrid.addRowAction("Edit", VaadinIcon.EDIT, person -> {
48+
editPerson(person);
49+
});
50+
51+
easyGrid.addRowAction("Delete", VaadinIcon.TRASH, ButtonVariant.LUMO_ERROR, person -> {
52+
personService.delete(person);
53+
grid.getDataProvider().refreshAll();
54+
}).withConfirmation("Are you sure you want to delete this person?");
55+
56+
// Actions as a context menu (overflow menu) instead of inline buttons
57+
easyGrid.setRowActionsAsMenu(true);
58+
59+
// Conditional visibility
60+
easyGrid.addRowAction("Activate", VaadinIcon.CHECK, person -> {
61+
personService.activate(person);
62+
}).withVisibleWhen(person -> !person.isActive());
63+
64+
easyGrid.addRowAction("Deactivate", VaadinIcon.CLOSE, person -> {
65+
personService.deactivate(person);
66+
}).withVisibleWhen(person -> person.isActive());
67+
68+
// Configure the actions column via the underlying Grid.Column
69+
easyGrid.getActionsColumn()
70+
.setHeader("Actions")
71+
.setWidth("150px")
72+
.setFrozenToEnd(true);
73+
```

0 commit comments

Comments
 (0)