|
| 1 | +# Studio "create a package and build in it" UX dogfood (browser) |
| 2 | + |
| 3 | +Goal: walk the **whole first-run authoring loop a new admin actually experiences** — |
| 4 | +boot the showcase example, log in, create a writable package in Studio, model an |
| 5 | +object with fields, enter a record, create an app with navigation, publish, and |
| 6 | +use the result as an end user — driving a real Chromium against `/_console` |
| 7 | +(vendored console build `7782698`, matching the `.objectui-sha` pin, so findings |
| 8 | +are not stale-bundle artifacts). Both sides of the read-only gate were exercised |
| 9 | +per ADR-0057 D10. |
| 10 | + |
| 11 | +Run: `objectstack dev --ui --seed-admin` on the showcase example, fresh SQLite DB, |
| 12 | +`admin@objectos.ai` seeded admin, headless Chromium via CDP with screenshots at |
| 13 | +every step. |
| 14 | + |
| 15 | +## The loop closes |
| 16 | + |
| 17 | +Home → **Build an app** → Studio landing → **+ 新建软件包** (`维修中心` / |
| 18 | +`com.example.repairs`) → Data pillar → object `Repair Ticket` (identifier |
| 19 | +auto-suggested from the English display name) → picklist field `Status` with 3 |
| 20 | +values → **Save draft** → Changes panel → **Publish** → record created in the |
| 21 | +runtime-faithful Records grid → **Create app** `Repair Center` (identifier |
| 22 | +auto-suggested) → Interfaces pillar → nav item bound to the object → **Publish** |
| 23 | +→ app appears in the Home launcher → end-user list renders the record with the |
| 24 | +picklist label chip. **Zero code, no dead ends, minutes end-to-end.** The |
| 25 | +draft→publish model reads consistently everywhere ("Unpublished draft" badge, |
| 26 | +`Changes · n` counter, "Published all drafts in this package (one atomic |
| 27 | +release)"). |
| 28 | + |
| 29 | +Other things that hold up well: |
| 30 | + |
| 31 | +- **Empty states teach the next step at every level** (object list, nav editor, |
| 32 | + records grid hint bar). |
| 33 | +- **Previews are the runtime renderer** ("Preview = runtime · same renderer") — |
| 34 | + nothing to un-learn between builder and app. |
| 35 | +- **Field type catalog** is deep (40+ types, grouped) and the properties panel |
| 36 | + covers advanced needs (CEL conditional-required, track history, indexed). |
| 37 | +- **Server enforces the writable/read-only gate**: creating an object in the |
| 38 | + source-loaded showcase package is rejected server-side even though the client |
| 39 | + let the gesture through (see finding 1). |
| 40 | +- Package switcher clearly badges **Read-only vs Writable** and offers "+ New |
| 41 | + package (writable base)". |
| 42 | + |
| 43 | +## Findings (ordered by user pain) |
| 44 | + |
| 45 | +### 1. Read-only packages accept edit gestures client-side, then fail late — P1 |
| 46 | + |
| 47 | +On `com.example.showcase` (Read-only badge shown), **"+ Add field" actually |
| 48 | +mutates the canvas** (header count went 16 → 17 fields, Field properties panel |
| 49 | +opened on `field_17`), and **"New object" opens the inline create form**. Only on |
| 50 | +the eventual server write does the user learn it was all futile — via a toast |
| 51 | +whose copy is developer-facing: *"read-only code/installed package… See |
| 52 | +docs/adr/0070-package-first-authoring.md"*. An internal ADR path is not an |
| 53 | +end-user remediation. |
| 54 | + |
| 55 | +Fix: client-side courtesy gating (disable/hide `Add field`, `New object`, |
| 56 | +`Save draft`, `Publish`, field-property inputs when the package is read-only, |
| 57 | +with a tooltip "Read-only package — create a writable package to edit"), and |
| 58 | +rewrite the server error copy to say what to *do* ("Switch to a writable package |
| 59 | +(top-left selector) or create one"), keeping the ADR pointer out of the UI. |
| 60 | + |
| 61 | +### 2. Field API name doesn't follow the label before first save — P1 |
| 62 | + |
| 63 | +"Add field" creates `field_2` / label "New field". Renaming the label to |
| 64 | +`Status` leaves the API name `field_2`, and the data column is then `field_2` |
| 65 | +forever (verified: the saved record stores `"field_2": "in_progress"`). Object |
| 66 | +and app identifiers *do* auto-suggest from the display name, so authors expect |
| 67 | +the same here and won't look at the API name input. |
| 68 | + |
| 69 | +Fix: sync API name from label while the field is new/unsaved (exactly the |
| 70 | +object-identifier behavior), lock it after first save. |
| 71 | + |
| 72 | +### 3. Publish is one click, no confirmation, and the Changes panel is too thin — P1 |
| 73 | + |
| 74 | +Publish fires immediately (toast: "Published all drafts in this package (one |
| 75 | +atomic release)") with no confirm step, and the Changes panel shows only |
| 76 | +`repair_ticket · New` — no field-level diff, no drill-in. Review-before-release |
| 77 | +is the panel's whole job (ADR-0016 §3.6 step 4), and one mis-click currently |
| 78 | +releases everything. |
| 79 | + |
| 80 | +Fix: Publish opens the pending-changes list with a confirm button; give the |
| 81 | +panel per-item detail (fields added/changed, updates that overwrite live). |
| 82 | + |
| 83 | +### 4. Mixed languages within single screens — P2 |
| 84 | + |
| 85 | +Login/Home are English; the Studio landing is Chinese (应用构建 / 新建软件包); |
| 86 | +the builder is English chrome with embedded Chinese strings (添加字段 / 未分组 / |
| 87 | +请选择… / 拖动字段排序…); the record form placeholder is 请选择… inside an |
| 88 | +otherwise-English dialog. Whatever the intended locale, half-translated screens |
| 89 | +read as unfinished. Audit the console's i18n keys for both locales; make the |
| 90 | +studio landing and canvas hint strings go through the same locale resolution as |
| 91 | +the rest of the console. |
| 92 | + |
| 93 | +### 5. New-package wizard: no ID suggestion, silent input munging, unexplained disabled button — P2 |
| 94 | + |
| 95 | +- Typing the name (维修中心) suggests nothing for the package ID (object/app |
| 96 | + identifiers do get suggestions — inconsistent). |
| 97 | +- Chinese names produce **no** identifier suggestion anywhere (no pinyin, no |
| 98 | + fallback), so zh-first users must guess the allowed charset. |
| 99 | +- Illegal characters are silently stripped while typing (`bad id!!` → `badid`). |
| 100 | +- With an invalid ID the create button is just disabled — no inline message |
| 101 | + explaining the required reverse-domain format (`com.example.repairs`). |
| 102 | + |
| 103 | +Fix: suggest an ID from the name (transliterate or fall back to `com.<tenant>.appN`), |
| 104 | +show the format rule inline when the button is disabled, and don't silently eat |
| 105 | +keystrokes — show what's invalid. |
| 106 | + |
| 107 | +### 6. A new app scaffolds no navigation — P2 |
| 108 | + |
| 109 | +"Create app" produces an app with zero nav items; the object the user just |
| 110 | +built isn't offered. The user must discover Interfaces → Edit → Add nav item → |
| 111 | +Label → Link to object. The create-app popover could offer "add existing |
| 112 | +objects as menu items" (checked by default) and pre-label the item with the |
| 113 | +object's plural label — that single step would have saved the entire manual |
| 114 | +wiring in this run. |
| 115 | + |
| 116 | +### 7. Records grid shows a duplicated "Actions" column in Studio — P3 |
| 117 | + |
| 118 | +The Studio Records tab (and its Interfaces preview) renders an `Actions` data |
| 119 | +column *and* the pinned row-actions column, both headed "Actions" |
| 120 | +(`#/Name/Status/Actions/Actions/+`). The end-user app shows only one. Likely the |
| 121 | +grid injects its own actions column without deduping against the runtime one. |
| 122 | + |
| 123 | +### 8. Verb/label inconsistencies around record creation — P3 |
| 124 | + |
| 125 | +Studio Records uses a big blue **Create** button adjacent to top-bar **Create |
| 126 | +app**; the runtime app calls the same action **New**. The record dialog's submit |
| 127 | +is **Create**. Pick one verb ("New" for records, keep "Create app" for apps) to |
| 128 | +de-collide the two adjacent buttons. |
| 129 | + |
| 130 | +### 9. Cold `/_console` load sits on a bare "Loading…" for ~8s — P3 |
| 131 | + |
| 132 | +First hit renders unbranded "Loading…" before redirecting to login. A logo + |
| 133 | +skeleton (or a faster auth probe) would make the first impression match the |
| 134 | +otherwise polished login screen. |
| 135 | + |
| 136 | +### 10. Small polish — P3 |
| 137 | + |
| 138 | +- Picklist value-editor inputs are so narrow their placeholders truncate |
| 139 | + ("valu" / "Labe"), and CJK labels show ~3 chars (待处理 → 待处…). |
| 140 | +- The read-only toast can appear twice for one gesture. |
| 141 | +- Interfaces canvas showed a ghost gray rectangle artifact below the fold on an |
| 142 | + empty app. |
| 143 | +- "Save draft" disables after a successful save (good) but there's no "last |
| 144 | + saved" timestamp anywhere. |
| 145 | + |
| 146 | +## Verdict |
| 147 | + |
| 148 | +The MVP loop of ADR-0016 §9 genuinely works first-try, and the |
| 149 | +draft→publish→run mental model comes across. The pain concentrates in three |
| 150 | +places: **read-only gating happens too late and speaks ADR** (finding 1), |
| 151 | +**identifier ergonomics** (findings 2 & 5, worst for CJK-named things), and |
| 152 | +**publish/review confidence** (finding 3). All are console-side (objectui) |
| 153 | +except the read-only error copy, which is the framework's dispatcher message. |
0 commit comments