Skip to content

feat: ClipboardReadAction + typed JsBuilder.callback primitive#24397

Merged
mshabarov merged 2 commits into
mainfrom
feature/trigger-step3
May 22, 2026
Merged

feat: ClipboardReadAction + typed JsBuilder.callback primitive#24397
mshabarov merged 2 commits into
mainfrom
feature/trigger-step3

Conversation

@Artur-
Copy link
Copy Markdown
Member

@Artur- Artur- commented May 21, 2026

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 $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<https://github.com/nullable T>)
  • <T> callback(TypeReference<T>, Consumer<https://github.com/nullable T>) for
    parameterised types (List<X>, Map<String, X>, …)
  • callback(SerializableRunnable) for the no-payload case

Each call registers a fresh ReturnChannelRegistration on 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 is
two 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)). The channelByNode map and inline dispatch method are gone; PromiseAction now has no state beyond the two optional consumers.

ClipboardReadAction is a direct Action subclass (not PromiseAction, because the resolved value is a typed payload, not just success/failure). It takes a SerializableConsumer<https://github.com/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(https://github.com/nullable String text, https://github.com/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").

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 21, 2026

Test Results

 1 420 files  +2   1 420 suites  +2   1h 21m 30s ⏱️ + 2m 55s
10 005 tests +5   9 936 ✅ +5  69 💤 ±0  0 ❌ ±0 
10 480 runs  +5  10 409 ✅ +5  71 💤 ±0  0 ❌ ±0 

Results for commit ca5930f. ± Comparison against base commit 3424caa.

♻️ This comment has been updated with latest results.

@Artur- Artur- force-pushed the feature/trigger-step3 branch 3 times, most recently from ce3b949 to 5043002 Compare May 21, 2026 15:19
@mshabarov mshabarov self-requested a review May 21, 2026 19:41
@Artur- Artur- force-pushed the feature/trigger-step3 branch from 5043002 to 0df325f Compare May 22, 2026 09:10
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").
@Artur- Artur- force-pushed the feature/trigger-step3 branch from 0df325f to 6b928db Compare May 22, 2026 09:49
@Artur- Artur- marked this pull request as ready for review May 22, 2026 09:50
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@mshabarov mshabarov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +22 to +23
text: string | null;
html: string | null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mshabarov mshabarov added this pull request to the merge queue May 22, 2026
Merged via the queue into main with commit 3e30222 May 22, 2026
55 of 57 checks passed
@mshabarov mshabarov deleted the feature/trigger-step3 branch May 22, 2026 13:02
Artur- added a commit that referenced this pull request May 22, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants