Skip to content

fix(ux): three first-week frictions where the app knew and did not say - #1706

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-1214-ux-polish-batch
Sep 6, 2026
Merged

fix(ux): three first-week frictions where the app knew and did not say#1706
os-steve merged 3 commits into
mainfrom
claude/issue-1214-ux-polish-batch

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Description

The polish batch off the end-user walkthrough — items 1, 5 and 6 shipped; items 2 and 7 stop-and-report, each with a criterion you can check without taking my word for it.

The first job on this card was re-measurement, not building: the five readings come from a browser walkthrough on 2026-08-20 against platform 17.1.0, and this branch is cut from a main that pins 17.3.0. Two of the five did not survive that gap intact.

Related Issues

Part of #1214 — deliberately not a closing keyword. Item 4 is still not done (it waits on its own dispatch now that #1206's fix has merged), and items 2 and 7 are returned for a ruling, so the card stays open.

Re-measurement, per item

item verdict what I shipped
1 — conversion is a double dialog still present removed the confirm
2 — new-lead form stops at 8 fields / owner renders empty half premise-dead, half refused by a standing ruling nothing — see below
5 — tasks carry three spellings of "done" still present, but the card's stated reason for the deletion is false dropped the percent from the default grid only
6 — related accordion defaults fully closed still present; the card's preferred fix is not expressible default-opened the first panel
7 — conversion stamps close_date = +90d silently still present; surfacing it trips both clauses of the ruling nothing — see below

Changes Made

Item 1 — the confirm in front of the conversion screen is gone.
convert_lead is flow-typed, and its target opens Conversion Details: a screen that states the decision, carries a Cancel button, and warns when the lead repeats one already on file. The dialog in front of it asked "Are you sure?" and carried nothing the next screen did not carry better.
Criterion: convert_lead declares no confirmText. That key alone is what the console's action runner gates the dialog on — measured on the pinned @objectstack/console 17.3.0 bundle, if (action.confirmText && !await confirmHandler(evaluate(action.confirmText))) return { success: false, error: 'Action cancelled by user' } — so its absence is the fix; there is no confirm: false to write.
Not weakened: ai.requiresConfirmation: true is untouched. The AI path consults that flag and nothing else and never read the dialog string, so an agent calling this action still reports that it needs human approval. The four locale bundles drop the now-sourceless confirmText with it.

Item 5 — the task grid stops spelling "done" three times.
⚠️ The card justified the deletion as "every seeded row is 0%". That is false and I re-measured it before deleting: two of the seven seeded tasks carry 100. What is true is the claim underneath — nobody maintains a percent per task — and the seed shows it more sharply than the card did: every value that exists is 100 and every one of those rows is status: 'completed'; task_completion writes the field only on the completed transition; and both in_progress rows sit at 0, the one status where a percent could say something a status cannot.
Criterion: on list.columns the percent is status re-rendered at lower resolution — it cannot separate not_started from in_progress, and the status column beside it can. Removing it loses no fact the row does not already carry twice.
Scope of the deletion, audited before cutting: list.columns only. Every other reader stays — task_gantt fills its bars from gantt.progressField, task_board and my_open_tasks show it (neither carries status and is_completed, so it is not a third spelling there), avg_progress in task.dataset.ts aggregates it, the hook and its pin are untouched, and the form's Effort section still lets a user author one. The three activities docs that enumerate the row are updated with it.
is_completed deliberately stays, and is not the other half of this: task_completion recomputes it from status on every insert and update, so it cannot contradict status — that is the card's "render completion from status", already done at the data layer — and it is this list's first sort key and its completion control.

Item 6 — the Related tab opens on its quotes.
Criterion: the first accordion item declares collapsed: false. That key is needed even though the schema defaults it to false, because the renderer opens panels by strict identity — items.filter(i => i.collapsed === false) — so an item omitting the key is undefined and stays shut. Measured on the pinned 17.3.0 bundle; the component's own designer text says the same ("collapsed: false opens a panel by default").
Only the first panel, and that is the renderer's choice not a preference: with allowMultiple absent it mounts as type="single" and takes defaultValue={open[0]}. Marking Products or Open Tasks too would change nothing on screen.
The card's first ask — "default-open the sections that have rows" — is not expressible and was not attempted. collapsed is a static boolean on a strict item shape (label / icon / collapsed / children; nothing else parses), the page is metadata evaluated before any row is read, and the related lists fetch their rows after mount. Reported instead of worked around, and the component is not this repo's to change.

Returned for a ruling

Item 2 — both halves stop here, for different reasons.

The half that matters (owner renders empty while the server defaults it) is refused by a standing guard. The mechanism is real and I measured it end to end: the console's create form seeds its defaults from the object schema, and prefills a user / lookup-to-sys_user field with the viewer's own id exactly when that field's defaultValue is the platform token current_user. A defaultValue on the form is not read by that path at all, so crm_lead.owner_id carrying the token is the only mechanism this repo has.
test/ownership-model.test.ts forbids it, twice, by name: "A defaultValue here would be the old model's mistake" and "nothing in the schema fills owner_id — the middleware stamp is the only filler", both citing #620. I implemented it, went red on those two assertions, and reverted rather than edit a guard that records a prior ruling.
⭐ Worth the maintainer's eye: the guard's stated reason is entirely about the server, and the card's complaint is entirely about the form. The guard therefore forecloses the only fix for a problem it never considered. The two are not obviously in conflict — the middleware would still do the stamping, and the token resolves to nothing on the user-less writes the guard is protecting — but that is a ruling to make, not one for this PR to assume.

The other half (the form "stops at 8 fields", add a collapsed "more fields" group) is premise-dead. Both console entry points resolve view.form ?? view.formViews.default and render the authored sections verbatim — measured on 17.3.0 here, and on 17.1.0 by the item-3 work, so it was not true at walkthrough time either. LeadViews.form has six sections carrying ~20 fields including title and industry, the two the card says are missing, and four of those sections are already collapsed: true, which is the collapsed group the card asks for. The 8 fields the card enumerates are formViews.quick_create, exactly — a form view no code path reaches: view.form wins, addRecord.formView is read by nothing, no form-typed action targets it, and navigation.view names detail_form. Building this item would mean adding a fifth collapsed group to a form that already has four, with no criterion to check it against.

Item 7 — surfacing close_date trips both clauses of the ruling, so I stopped.
Why it matters, first: close_date is what puts an opportunity in a forecast period. A conversion stamps +90 days and shows the rep nothing, so a deal the rep expects next month is filed a quarter out — and it moves the forecast invisibly, because the number nobody saw is the number the forecast believes. This is worth doing.
The ruling said to note that surfacing it widens the screen's field contract, and to stop and report if it touches a server-side required-field validation. Both happened, measured rather than predicted — I built it, ran it, and reverted:

  • test/lead-duplicate-visibility.test.ts pins the screen's exact field list ("still collects the conversion inputs it always did") and went red with + "closeDate". The contract is pinned on purpose.
  • test/flow-variable-conditions.test.ts, running a real ObjectQL engine, went red with Node 'create_opportunity' failed: create_record(crm_opportunity) failed: Close Date is required — because crm_opportunity.close_date is required + notNull, and once the screen owns the value, any resume that omits it fails the create.

The working design is on the branch's scratch (not in this diff) and is small: declare a closeDate input variable, put the screen field at defaultValue: '{TODAY() + 90}' — server-interpolated, so the +90 stays written exactly once — and read {closeDate} in create_opportunity. The console posts it whether or not the user touches it (the runner seeds its value state from every field carrying a defaultValue), so the console path is safe; it is the other callers, and the two pins, that need a decision.

Premise changes since the dispatch was written

Testing

pnpm verify — the full chain (validate, typecheck, lint, lint:i18n-gate, hygiene, hygiene:tokens, build, test) — exit 0 on the tree committed here.

Test Files  161 passed (161)
     Tests  3408 passed | 1 skipped (3409)

Source token ratchet clean: business semantics ~84,483 (ceiling ~100,000) · interaction layer ~37,958 (ceiling ~40,000) · authored total ~136,782 (ceiling ~140,000).

No new tests and no new gates, per the dispatch. No test file is edited in this diff: the two suites that went red are the ones that stopped items 2 and 7, and reverting those items is what makes them green again rather than an edit to them. Items 1, 5 and 6 needed no browser — each was settled by reading the renderer that decides it, which is a stronger reading than a screenshot.

Changeset: .changeset/first-week-frictions-say-what-the-app-knows.md.


Generated by Claude Code

…on screen

`convert_lead` is a flow-typed action whose target opens the `Conversion
Details` screen — the screen that states the decision, carries a Cancel
button, and warns when this lead repeats one already on file. The
`confirmText` dialog in front of it asked "Are you sure?" and carried
nothing the next screen did not carry better, so it cost every conversion
an extra click to answer a question it could not help anyone answer.

Measured on the pinned @objectstack/console 17.3.0 bundle rather than
assumed: the action runner gates the dialog on the key's presence alone
(`if (action.confirmText && !await confirmHandler(...))`), so removing the
key is what removes the dialog.

`ai.requiresConfirmation` is untouched and unaffected — the AI path reads
that flag and nothing else, and never read the console's dialog string, so
an agent calling this action still reports that it needs human approval.
The four locale bundles drop the now-sourceless `confirmText` with it, and
the conversion flow's note about where the duplicate warning lives stops
describing the removal as still pending.

Confirms stay where they earn one: `close_case` and `escalate_case` are
destructive with no follow-up UI.

Co-authored-by: Claude
The All Tasks grid rendered completion three ways on one row — the
`is_completed` tick, `status`, and `progress_percent` — and the percent was
the one carrying nothing. Re-measured against the seed rather than taken on
description: every value that exists is 100 and every one of those rows is
`status: 'completed'`; `task_completion` writes the field only on the
completed transition; and both `in_progress` rows sit at 0, the one status
where a percent could say something a status cannot. On this grid it is
`status` re-rendered at lower resolution — it cannot separate `not_started`
from `in_progress`, which the column beside it can.

Removed from `list.columns` only. The field and every real reader stay:
`task_gantt` fills its bars from `gantt.progressField`, `task_board` and
`my_open_tasks` show it (neither carries `status` AND `is_completed`, so it
is not a third spelling there), `avg_progress` aggregates it, and the form's
Effort section still lets a user author one.

`is_completed` stays too, and is not the other half of this: the hook
recomputes it from `status` on every write, so it cannot contradict it —
that is "render completion from status", already done at the data layer —
and it is this list's first sort key and its completion control.

The three activities docs drop `progress` from the row they enumerate.

Co-authored-by: Claude
The related accordion mounted with all three panels shut, so the tab opened
on three closed bars whose headers already carried the counts — the click
bought nothing the page had not already said.

`collapsed: false` on the first item is the whole mechanism, and it is
needed even though the schema defaults the key to `false`: the renderer
opens panels by strict identity (`items.filter(i => i.collapsed === false)`),
so an item that omits the key is `undefined` and stays shut. Measured on the
pinned @objectstack/console 17.3.0 bundle; the component's own designer text
says the same.

Only the first panel, and that is the renderer's choice rather than a
preference: with `allowMultiple` absent the accordion mounts as
`type="single"` and takes `defaultValue={open[0]}`. Marking the other two
would change nothing on screen.

Adds the changeset for this batch.

Co-authored-by: Claude
@vercel

vercel Bot commented Sep 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Sep 6, 2026 12:24pm UTC

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Server-side behaviour — hooks, flows, actions metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants