feat(forms): send a client a request-form link #487

Merged
longjacksonle merged 2 commits from feat/PMS-730-send-request-link into main 2026-08-06 21:45:58 +02:00

Third slice of the client-request frontend, after the public page (#485) and the builder (#486): the control that actually sends a client a form.

A Request forms card on the company detail page shows what has been sent to this client and what became of each link, with a "Send a form" action beside it.

Why the company, not the form

The workflow is "I am looking at this client and I need something from them", not "I am looking at a form and wondering who to send it to". The company detail page is where an agent already is when that thought occurs.

Details worth reviewing

Two things are filtered out of the pickers rather than offered and then rejected. Retired forms refuse submissions server-side, so offering one would issue a link that dies on arrival. Contacts with no email address cannot receive anything, so choosing one could only produce a 400 telling the agent to supply an address they could have typed in the first place.

Choosing a contact fills the address in but leaves it editable, so the agent can see where this is actually going and can still override it for a one-off. The server treats an explicit address as winning over the contact's, which is what the help text says.

The contact picker fetches its own list. Reusing the detail page's contacts_resource would have been less code, but it is capped at per_page=5 for its preview card, so every contact past the fifth would have been silently missing from the picker.

The card owns its own resource. Every other card on that page takes a Resource prop threaded from the detail page. This one does not, so composing it costs one line in contacts.rs and it refreshes itself after a send. contacts.rs is already the largest page in the repo at ~4.8k lines, so the card lives in its own module.

Submitted beats Expired in the status badge. A link that was used and has since passed its expiry is still a request that came in; showing "Expired" would read as though the client never replied. Unit-tested, along with the other two states.

No token anywhere. The server never returns one and this surface never asks. A link that needs resending is reissued.

Verification

cargo check --target wasm32-unknown-unknown, cargo fmt --all --check, cargo test (261 passed, 0 failed, including 3 new unit tests), and both guard scripts.

The two pre-existing clippy failures in src/pages/projects.rs and src/pages/time.rs are unchanged and unrelated; see #485 and #486. They are now the only thing standing between this repo and a clean just check on a current toolchain, so they are worth a two-line PR of their own.

Next

The measured-duration readout on the KB article page, which closes the loop.

Third slice of the client-request frontend, after the public page (#485) and the builder (#486): the control that actually sends a client a form. A **Request forms** card on the company detail page shows what has been sent to this client and what became of each link, with a "Send a form" action beside it. ## Why the company, not the form The workflow is "I am looking at this client and I need something from them", not "I am looking at a form and wondering who to send it to". The company detail page is where an agent already is when that thought occurs. ## Details worth reviewing **Two things are filtered out of the pickers rather than offered and then rejected.** Retired forms refuse submissions server-side, so offering one would issue a link that dies on arrival. Contacts with no email address cannot receive anything, so choosing one could only produce a 400 telling the agent to supply an address they could have typed in the first place. **Choosing a contact fills the address in but leaves it editable**, so the agent can see where this is actually going and can still override it for a one-off. The server treats an explicit address as winning over the contact's, which is what the help text says. **The contact picker fetches its own list.** Reusing the detail page's `contacts_resource` would have been less code, but it is capped at `per_page=5` for its preview card, so every contact past the fifth would have been silently missing from the picker. **The card owns its own resource.** Every other card on that page takes a `Resource` prop threaded from the detail page. This one does not, so composing it costs one line in `contacts.rs` and it refreshes itself after a send. `contacts.rs` is already the largest page in the repo at ~4.8k lines, so the card lives in its own module. **Submitted beats Expired in the status badge.** A link that was used and has since passed its expiry is still a request that came in; showing "Expired" would read as though the client never replied. Unit-tested, along with the other two states. **No token anywhere.** The server never returns one and this surface never asks. A link that needs resending is reissued. ## Verification `cargo check --target wasm32-unknown-unknown`, `cargo fmt --all --check`, `cargo test` (261 passed, 0 failed, including 3 new unit tests), and both guard scripts. The two pre-existing clippy failures in `src/pages/projects.rs` and `src/pages/time.rs` are unchanged and unrelated; see #485 and #486. They are now the only thing standing between this repo and a clean `just check` on a current toolchain, so they are worth a two-line PR of their own. ## Next The measured-duration readout on the KB article page, which closes the loop.
Mirrors the shapes mokosh-server returns from `/form-request-links`.

`RequestLink` has no token field, and the server never sends one: the token is a credential for the recipient, and echoing it into an agent response would put it in logs and browser history. A link that needs resending is reissued, not recovered.

`status()` folds the two timestamps into the three states an agent cares about, and lets Submitted win over Expired: a link that was used and has since passed its expiry is still a request that came in, so reporting it as expired would read as though the client never replied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011myibMMwyb6za3GVWJGkiX
feat(forms): send a client a request-form link from their company
All checks were successful
Check / fmt + clippy + tests (pull_request) Successful in 1m39s
Create release / Create release from merged PR (pull_request) Has been skipped
e735f4dcb4
Adds a Request forms card to the company detail page: what has been sent to this client, what became of each link, and the control to send another.

Placed on the company rather than on the form, because the workflow is "I am looking at this client and I need something from them" rather than "I am looking at a form".

In its own module rather than in `contacts.rs`, which at ~4.8k lines is already the largest page in the repo. The card owns its own resource, so composing it costs the detail page one line and it can refresh itself after a send instead of having a resource threaded down.

Two things are filtered out of the pickers rather than being offered and then rejected: retired forms, which refuse submissions server-side and would issue a link that dies on arrival, and contacts with no email address, where choosing one could only produce a 400 telling the agent to type an address they could have typed anyway. Choosing a contact fills the address in but leaves it editable, so the agent can see where this is actually going and can still override it.

The contact picker fetches its own list rather than reusing the detail page's contacts resource, which is capped at 5 rows for its preview card and would have silently hidden every other contact from the picker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011myibMMwyb6za3GVWJGkiX
longjacksonle deleted branch feat/PMS-730-send-request-link 2026-08-06 21:45:58 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-apps!487
No description provided.