feat: ClipboardReadAction + typed JsBuilder.callback primitive#24397
Conversation
ce3b949 to
5043002
Compare
5043002 to
0df325f
Compare
Adds a typed server-callback primitive to the trigger framework and the first action that needs it: reading the user's clipboard. `JsBuilder.callback(...)` — two overloads — allocates a `$N` placeholder for a JS function `(payload) => …` that, when called from client JS, deserialises its first argument to T via Jackson and invokes the handler on the UI thread: - `<T> callback(Class<T>, Consumer<@nullable T>)` - `<T> callback(TypeReference<T>, Consumer<@nullable T>)` for parameterised types (`List<X>`, `Map<String, X>`, …) Each call registers a fresh `ReturnChannelRegistration` on the trigger's host node; channels are not deduplicated across calls. Channels go away when the host node detaches. `ClipboardReadAction` is a direct `Action` subclass (not `PromiseAction`, because the resolved value is a typed payload, not just success/failure). It takes a `SerializableConsumer<@nullable ClipboardPayload>` handler that receives the clipboard contents on success or `null` on any failure (rejection, empty items, unsupported API). The handler is single because clipboard read failures rarely carry useful detail — callers who need success/error distinction can switch to PromiseAction-style. `ClipboardPayload(@nullable String text, @nullable String html)` is the typed payload — first ClipboardItem's `text/plain` and `text/html` representations. The actual clipboard read + `{text, html}` extraction lives in `flow-client/src/main/frontend/Clipboard.ts`, mirroring `Geolocation.ts`: it attaches a `readPayload()` function to `window.Vaadin.Flow.clipboard`, imported by `Flow.ts` so it loads with the framework. The Action's `appendStatement` just renders the routing glue — `window.Vaadin.Flow.clipboard.readPayload()` `.then(p=>\$N(p)).catch(()=>\$N(null))` — keeping the multi-statement JS out of Java string concatenation. The IT (`TriggerClipboardReadView`/`IT`) shims `navigator.clipboard.read` with a fake `ClipboardItem` that resolves the two MIME types and a rejecting variant, exercising the TS layer end-to-end and asserting the server-side status div reflects the payload (or "null").
0df325f to
6b928db
Compare
|
mshabarov
left a comment
There was a problem hiding this comment.
Couple of things to think about, but not blocking.
| * | ||
| * For internal use only. May be renamed or removed in a future release. | ||
| */ | ||
| public class ClipboardReadAction extends Action { |
There was a problem hiding this comment.
One possible potential improvement could be if ClipboardReadAction would extend PromiseAction and PromiseAction would have a generic type. JsonNode then would not leak into app code through a Success wrapper; every subclass won't write an adapter to unwrap it.
What would get better:
- PromiseAction — payload type is generic; framework Jackson-decodes once, onSuccess is Consumer<@nullable T>.
- Success is gone — one fewer wrapper.
- Subclasses drop their adapters — CopyTextToClipboardAction extends PromiseAction; RequestFullscreenAction extends PromiseAction.
- Type-safe at the API surface — the compiler enforces the payload shape; JsonNode never leaks.
- One pattern for value-returning promises — future built-ins (clipboard read, share, file picker) reuse PromiseAction instead of inventing adapters.
Then:
ClipboardReadAction extends PromiseAction<ClipboardPayload>
CopyTextToClipboardAction extends PromiseAction<String>
RequestFullscreenAction extends PromiseAction<Void>
And there one "but": this is an internal API so far, so no high priority and maybe no need for now - just something to think about in the code design.
| text: string | null; | ||
| html: string | null; |
There was a problem hiding this comment.
In the future we may want to add more content types (already have plans for images), this can bring incompatible changes. But not blocking this intermediate low level PR I think.
Addresses the design suggestion from PR #24397 review: lift the payload type into the PromiseAction class signature so subclasses don't have to write per-action adapters and JsonNode no longer leaks through Success.value(). PromiseAction<T> takes the payload type in its with-outcome constructor and Jackson-decodes the resolved value once before invoking onSuccess. The public Success<JsonNode> wrapper is gone — onSuccess is SerializableConsumer<@nullable T> directly. The wire shape (Outcome{ok, value, error}) is unchanged; only the dispatch seam moves. Subclasses become trivial: - CopyTextToClipboardAction extends PromiseAction<String> — drops adaptOnCopied / asString; onCopied receives the decoded String. - RequestFullscreenAction extends PromiseAction<Void> — keeps the SerializableRunnable convenience via a one-line adapter to Consumer<@nullable Void>. - ClipboardReadAction extends PromiseAction<ClipboardPayload> — was a direct Action subclass with a single-handler-null-on-failure API; now adopts the two-handler shape so callers can distinguish "empty clipboard" (onPayload(null)) from "permission denied" (onError). JsBuilder.callback(Class<T>, …) / callback(TypeReference<T>, …) were added in step3 to support ClipboardReadAction's direct-Action shape. With ClipboardReadAction folded into PromiseAction<T>, no consumer remains, so they're removed — keeping the primitive surface tight. The IT view's onError branch sets "error=" + err.name(); the rejecting-shim IT case is renamed to assert that path instead of the former "null" propagation.



Adds a typed server-callback primitive to the trigger framework and the first action that needs it: reading the user's clipboard.
JsBuilder.callback(...)— three overloads — allocates a$Nplaceholder for a JS function(payload) => …that, when called from client JS, deserialises its first argument to T via Jackson and invokes the handler on the UI thread:<T> callback(Class<T>, Consumer<https://github.com/nullable T>)<T> callback(TypeReference<T>, Consumer<https://github.com/nullable T>)forparameterised types (
List<X>,Map<String, X>, …)callback(SerializableRunnable)for the no-payload caseEach call registers a fresh
ReturnChannelRegistrationon the trigger's host node; channels are not deduplicated across calls (the previous per-(action, host) dedup in PromiseAction was a micro-opt that didn't survive generalising the primitive). Channels go away when the host node detaches.PromiseAction is rewritten on top of
callback(...). Its previous single-channel(boolean, message)tuple dispatch istwo callbacks — a runnable for the success leg, a String-typed consumer for the error leg — and the rendered JS becomes
.then(()=>$ok()).catch(e=>$err(msg)). ThechannelByNodemap and inline dispatch method are gone; PromiseAction now has no state beyond the two optional consumers.ClipboardReadActionis a directActionsubclass (notPromiseAction, because the resolved value is a typed payload, not just success/failure). It takes aSerializableConsumer<https://github.com/nullable ClipboardPayload>handler thatreceives the clipboard contents on success or
nullon any failure (rejection, empty items, unsupported API). The handler is single because clipboard read failures rarely carry useful detail — callers who need success/error distinction can switch to PromiseAction-style.ClipboardPayload(https://github.com/nullable String text, https://github.com/nullable String html)is the typed payload — first ClipboardItem'stext/plainandtext/htmlrepresentations.The actual clipboard read +
{text, html}extraction lives inflow-client/src/main/frontend/Clipboard.ts, mirroringGeolocation.ts: it attaches areadPayload()function towindow.Vaadin.Flow.clipboard, imported byFlow.tsso it loadswith the framework. The Action's
appendStatementjust renders the routing glue —window.Vaadin.Flow.clipboard.readPayload().then(p=>$N(p)).catch(()=>$N(null))— keeping the multi-statementJS out of Java string concatenation.
The IT (
TriggerClipboardReadView/IT) shimsnavigator.clipboard.readwith a fakeClipboardItemthat resolvesthe two MIME types and a rejecting variant, exercising the TS layer end-to-end and asserting the server-side status div reflects the payload (or "null").