fix(ux): three first-week frictions where the app knew and did not say - #1706
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
os-steve
marked this pull request as ready for review
September 6, 2026 12:29
This was referenced Sep 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
mainthat 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
close_date = +90dsilentlyChanges Made
Item 1 — the confirm in front of the conversion screen is gone.
convert_leadis flow-typed, and its target opensConversion 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_leaddeclares noconfirmText. That key alone is what the console's action runner gates the dialog on — measured on the pinned@objectstack/console17.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 noconfirm: falseto write.Not weakened:
ai.requiresConfirmation: trueis 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-sourcelessconfirmTextwith 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_completionwrites the field only on the completed transition; and bothin_progressrows sit at 0, the one status where a percent could say something a status cannot.Criterion: on
list.columnsthe percent isstatusre-rendered at lower resolution — it cannot separatenot_startedfromin_progress, and thestatuscolumn beside it can. Removing it loses no fact the row does not already carry twice.Scope of the deletion, audited before cutting:
list.columnsonly. Every other reader stays —task_ganttfills its bars fromgantt.progressField,task_boardandmy_open_tasksshow it (neither carriesstatusandis_completed, so it is not a third spelling there),avg_progressintask.dataset.tsaggregates it, the hook and its pin are untouched, and the form's Effort section still lets a user author one. The threeactivitiesdocs that enumerate the row are updated with it.is_completeddeliberately stays, and is not the other half of this:task_completionrecomputes it fromstatuson 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 tofalse, because the renderer opens panels by strict identity —items.filter(i => i.collapsed === false)— so an item omitting the key isundefinedand 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
allowMultipleabsent it mounts astype="single"and takesdefaultValue={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.
collapsedis 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_userfield with the viewer's own id exactly when that field'sdefaultValueis the platform tokencurrent_user. AdefaultValueon the form is not read by that path at all, socrm_lead.owner_idcarrying the token is the only mechanism this repo has.test/ownership-model.test.tsforbids it, twice, by name: "AdefaultValuehere would be the old model's mistake" and "nothing in the schema fillsowner_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.defaultand 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.formhas six sections carrying ~20 fields includingtitleandindustry, the two the card says are missing, and four of those sections are alreadycollapsed: true, which is the collapsed group the card asks for. The 8 fields the card enumerates areformViews.quick_create, exactly — a form view no code path reaches:view.formwins,addRecord.formViewis read by nothing, no form-typed action targets it, andnavigation.viewnamesdetail_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_datetrips both clauses of the ruling, so I stopped.⭐ Why it matters, first:
close_dateis 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.tspins 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 withNode 'create_opportunity' failed: create_record(crm_opportunity) failed: Close Date is required— becausecrm_opportunity.close_dateisrequired+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
closeDateinput variable, put the screen field atdefaultValue: '{TODAY() + 90}'— server-interpolated, so the +90 stays written exactly once — and read{closeDate}increate_opportunity. The console posts it whether or not the user touches it (the runner seeds its value state from every field carrying adefaultValue), 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
22403e99, which contains both. Neither collides with what shipped here.Testing
pnpm verify— the full chain (validate,typecheck,lint,lint:i18n-gate,hygiene,hygiene:tokens,build,test) — exit 0 on the tree committed here.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