Skip to content

Commit 18870c9

Browse files
feat(console)!: an administrator's first-run checklist on Home replaces the currency and time zone dialog (#30868)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoPNS2FEPAcwYy3HAGUQ4J7c)_ Fixes #30206 Clause-②: yes (narrowing) — the published `@object-ui/i18n` packs (all ten) drop the 14 `console.workspaceTimezonePrompt.*` keys, which had no reader outside the removed dialog. The widening half: new console i18n keys `console.firstRunChecklist.*` and `console.settingsView.invalidFormat` / `.invalidFormatHelp` / `.invalidFormatPick` (all ten packs) Metadata-protocol: none Supersedes #30552 (same change, taken over from a branch whose account is out of tokens: cut from `claude/project-thread-5murn7` @ `2dc81e70`, then `origin/main` merged). ## 维护者速读 - 改了什么:管理员打开首页(/home)时,顶部出现一张「完成初始设置」清单(组织名称、默认语言和国家、系统时区、默认货币、邮件发送、邀请同事),原来一进系统就弹出的时区/货币对话框删掉了;在默认国家里输入「中国」现在能保存为 CN,格式错误的提示改成读者语言。 - 为什么改:维护者原话「加一个首次配置清单:目前启动时就弹出选择货币很奇怪,一起处理」;新部署的管理员不知道先设什么,默认语言、国家和货币还互相矛盾。 - 风险与回滚:只动控制台、语言包和两处 e2e 准备脚本,不动服务端;清单只读设置,只有点「确认」才写,写的就是设置页同一个 PUT。语言包删掉对话框的 14 个键,属 TypeScript 层面的收窄,changeset 写明了迁移。首屏英文语言包 57,233 / 60,000 gz,上限未动。回滚即 revert 本 PR,弹窗回来。 - 先看:`ui/apps/console/src/pages/settings/FirstRunChecklist.tsx`(读取与只在确认时写入)、`firstRunChecklistLogic.ts`(每行何时算完成)、`SettingsField.tsx` 的 `optionByName`(输入名称匹配代码)。 - 两分钟验证:`os dev --fresh` 后用管理员登录,打开 /home,顶部应有清单;中文浏览器下点「默认语言和国家/地区」的「设置」,在国家框输入「中国」后点「确认」,应提示已保存、该行打勾,Setup 页不再弹任何对话框。 - 需要你做的:改动 4,694 行(42 个文件,+2,671 / −2,023),超过 3,000 行人工合并线,请在席位复核通过后批准合并。 ## What changes **The dialog is gone; Home carries an administrator's first-run checklist.** `WorkspaceTimezonePrompt.tsx` and its mount in `AppContent.tsx` are removed. `FirstRunChecklist` is drawn at the top of Home by the console's Home route (`App.tsx`), above the page's own sections, inside the `ConsolePermissionsScope` that route already sits under. Its logic, `firstRunChecklistLogic.ts`, is the dialog's logic widened to every row (git shows it as a delete and an add; it keeps `browserTimeZone`, `browserLanguage`, `browserCurrency`, `isAdmissibleValue` and `mayWrite`). **Row list** (the card leaves it to the PR; the seat kept Home as the only placement). Each row's state is read from the real setting every time it is drawn, never from a click: | # | Row | Done when | Action | |---|---|---|---| | 1 | Organization name | the active organization's name is no longer `Default Organization`, the name plugin-auth gives it (#30125 row 1) | link to Setup → Organization record | | 2 | Default language and country | `localization.locale` and `.default_country` both resolve from a rung other than the manifest default | edit in place, pre-filled from the browser | | 3 | System timezone | `localization.timezone` resolves from a rung other than the default | edit in place, pre-filled with the browser zone | | 4 | Default currency | `localization.currency` holds any value (it has no platform default) | edit in place, pre-filled with the browser region's currency | | 5 | Email delivery | `mail.provider` is not `log`, `from_email` is set, and SMTP has `smtp_host`; shown only to a session that may write mail | link to Setup → Mail Delivery | | 6 | Invite colleagues | a second member or a pending invitation | link to Setup → Users (Invite User) | A done row shows its value; a row whose state cannot be read (mail unreadable, members unreadable) is left out rather than shown as not done. Once every row shown is done the checklist draws nothing, and it comes back on its own if a setting is unset again. **The rulings the dialog was built on still hold** (objectui#11693 B2, #28872). The browser only pre-fills rows 2 to 4. Nothing is written until the row's Confirm, which is `saveSettingsNamespace('localization', changes)`, the Settings page's own `PUT /api/settings/localization`. Cancel writes nothing. An unset currency stays on the list until it is set. Language, country and currency are one offer read from the browser's first language (`zh-CN` → 中文, CN, CNY), so they cannot contradict each other. **Who sees it.** A session that may write `localization` (its manifest's `writePermission`, failing closed on an unreported answer). A member sees nothing and sends one `GET /api/settings` (the list, which never refuses) and no namespace read. The close button hides it for that administrator, in that workspace, on that device (`localStorage` `os:first-run-checklist:ORG:USER`); nothing is written to the server and no row changes. Its reads wait for the console's first paint (`whenBootPainted`, objectui#12980), as the dialog's did. **「中国」 in 默认国家.** The refusal started in the console: `DomainCombobox` swapped typed text for a code only when it equalled the option label (「中国 (CN)」) or the code, so 「中国」 went to the server as typed, and the server's two-letter pattern refused it with `invalid_format` in English. Now a typed name that exactly one option carries (the label without its own code beside it, trimmed, case-insensitive) is committed as that option's code; any other text is still committed verbatim and the server stays the judge. A refusal with `code: 'invalid_format'` is now drawn in the console's words and the reader's language, naming the setting by the label its row draws (`settingsFieldRefusals.ts`): "Choose a value for Default country from the list." for a picker, and "Gateway model is not in a valid format. Use provider/model." for a typed field, repeating the field's own help line because the refusal takes its place. Every other refusal code keeps the server's sentence. The server's pattern check is unchanged (⛔ `packages/**`). **The narrowing.** `@object-ui/i18n`'s exported `TranslationKeys` (`typeof en`) loses the 14 `console.workspaceTimezonePrompt.*` keys in all ten packs. The changeset is `feat(console)!`, carries the BREAKING note, the removed-key table and the ADR-0087 disposition `not-required (runtime-interface-only ui/packages/i18n/src/locales/en.ts#TranslationKeys)`, as on PR #29359. **Eager language pack.** The checklist's strings sit in the console group of the eager packs. After PR #30640 moved the marketplace pages' strings into the lazy `marketplace` namespace, `i18n-locale-en` measures 57,233 gz against its unchanged 60,000 ceiling on this branch. This PR adds no marketplace-page strings, so nothing moves into that namespace. **Files beyond the claim's first surface**, each a reader of what was removed (claim amendments 2 and 3): `ui/apps/console/src/pages/auth/SetupPage.tsx` (imports `browserTimeZone` from the renamed module), `ui/apps/console/src/loadLocales.ts` (comment), the four console tests that mocked the dialog's module, `ui/e2e/live/global-setup.ts` and `examples/app-showcase/e2e/global-setup.ts` (each waited up to 60 s for the dialog's testid before a run; the checklist is not modal, so that step is removed), `docs/qa/platform-checklist/RUNNER.md` (told QA runners to dismiss the dialog), and three i18n pins that named the dialog's keys or file (`machineLocaleCensus-9909`, `console-copy-system-noun-12956`) or count the de pack's quotes (`de-quote-pairing-3876`, 105 → 107 for two new „…“ pairs). ## Tests - `ui/apps/console/src/pages/settings/__tests__/FirstRunChecklist.test.tsx` (10), on the real `api.ts` against a stubbed `fetch` and the real `MePermissionsProvider`: a member reads the list only and sees nothing; no capability answer or an empty one sends no request; a fresh workspace shows all six rows not done with their links and writes nothing; rows the server reports as set are done and a fully set workspace draws nothing; Set pre-fills the currency from the browser, nothing is written until Confirm, and Confirm sends exactly one PUT with `{ currency: 'CNY' }`; language and country are one offer (`{ locale: 'zh-CN', default_country: 'CN' }`); Cancel and the close button write nothing and the hide survives a remount with no request; an `invalid_format` refusal is shown in the console's words and the row stays not done; zh copy; no read while the boot splash is up. - `firstRunChecklistLogic.test.ts` (11): `localeOffer`, `browserRegion`, the admissible pre-fill, `isKeySet`, row done, `isMailConfigured`, `isOrganizationNamed`, `hasInvitedColleagues`, the dismissal record and unreadable storage. - `SettingsField.domainOptionTypedName-30206.test.tsx` (5): 「中国」 commits `CN`; trimmed and case-insensitive with full-width brackets; a name two options carry and part of a name stay verbatim; a currency name commits its code. - `settingsFieldRefusals-30206.test.ts` (5), against the shipped `en` templates. - Updated: `SettingsView.field-errors`, `SettingsView.crypto-unavailable` and `SettingsView.refusalToast-13146` asserted the server's English sentence for `invalid_format`; they now assert the console's sentence (and that the server's is gone). - Removed with the dialog: `WorkspaceTimezonePrompt.test.tsx`, `.currency-28872.test.tsx`, `.afterBootPaint-12980.test.tsx`; their pins (only Confirm writes, the browser zone pre-fill, the currency arm, the boot-paint wait, members never asked) are carried by the new files above. ## Acceptance notes - **Placement.** Home only, by the seat's call (claim amendment 2 on the card). Home is the environment's own home (ObjectUI ADR-0075) and every deployment that declares no landing app opens there after sign-in. A deployment whose app declares `isDefault` lands its administrator in that app, and the checklist is then seen on the next visit to Home, not at sign-in. - `DEFAULT_ORGANIZATION_NAME` is a copy of plugin-auth's literal (`ensure-default-organization.ts`). No test holds the two equal: a `ui/` test reading `packages/**` source would be a cross-package input outside both CI rosters. A rename on the server side would make row 1 read "named" for an organization nobody named. - Refusal codes other than `invalid_format` (for example `value_domain`) still carry the server's English sentence; localizing `FieldError.message` is a server concern and out of this card. - The dialog's old "asked once" record (`os:workspace-timezone-prompt:*`) is no longer read; it stays in browsers that have it, harmlessly. - `packages/plugins/plugin-auth/src/dev-admin-seed-workspace-timezone.test.ts` still describes the dialog in its doc comment (the condition it pins is still the one row 3 reads). Left alone: ⛔ `packages/**` on this card. - objectui#11693 is outside this session's repository scope; its ruling B2 is quoted from the code comments that carried it. - The branch is the card thread's platform-assigned `claude/project-thread-03bt30`, not `claude/issue-30206-…`. The eight commits carried over from #30552 name the card in their subjects; landed-branch history is not rewritten. - #30125 rows 1 and 3 are covered here, as the card says. ## Verification All readings on head `e823d8fd9d` unless a line names another. <details><summary>Eager closure (Console vite build, VITE_BASE_PATH=/console/)</summary> - `pnpm --dir ui run check:eager-closure` exit 0: eager closure 2157.8 KB gz across 103 of 2412 chunks (budget 2431.6 KB). `i18n-locale-en` 57,233 gz (from `ui/apps/console/dist/eager-closure.json`) against the unchanged 60,000 ceiling, headroom 2,767. The checker's ceilings are byte-identical to `main` (no diff to `check-eager-closure-budget.mjs`). </details> <details><summary>Tests and type-check</summary> - `pnpm --dir ui exec vitest run --maxWorkers=2 apps/console/ packages/i18n/` (lock VERDICT command-exit 0): Test Files 402 passed (402), Tests 4540 passed, 13 skipped (4553). - `pnpm --filter @object-ui/i18n type-check`, `pnpm --filter @object-ui/console type-check`, `pnpm --dir ui run type-check:e2e`, `pnpm --filter @objectstack/example-showcase typecheck`: all four exit 0 (one locked run, VERDICT command-exit 0). - `pnpm --dir ui run check:new-line-citations`: 0 new citations. </details> <details><summary>Full dogfood battery (Clause-② narrowing arm)</summary> - `pnpm --filter @objectstack/dogfood test`, unsharded, after a full `turbo run build --filter='!@objectstack/docs'` (118 of 118 tasks): lock VERDICT command-exit 0, Test Files 424 passed, 3 skipped (427), Tests 3538 passed, 41 skipped (3579), no failures. </details> <details><summary>Derived gates: node scripts/pm/dispatch-gates.mjs --commands (58) and --ran</summary> - 58 of 58 exit 0, each run with stdin closed and its exit code recorded before any pipe. `pnpm --dir ui exec vitest run scripts/` first answered 99 (lock not reached while the console tests held it) and was re-run under the same slot: Test Files 170 passed, 2 skipped; Tests 4979 passed, 2 skipped. - `--ran`: `✓ dispatch-gates --ran: 58 derived famil(ies) accounted for — 58 run, 0 NOT-MEASURED`. - Changed lines per the derivation: 4,694 (+2,671 / −2,023, 42 files), over the 3,000 human-merge threshold. </details> <details><summary>Live: before on main, after on the branch (taken on #30552 head 9914c31; no console source changed since)</summary> - Before, on `main` `fa9c0aa2`: entering Setup opened the currency dialog over the page, zh and en. - After, zh browser (`zh-CN`, `Asia/Shanghai`): `/home` shows the checklist, progress `已完成 2/6` (the rig's dev seed sets the zone and seeds members). Opening 默认语言和国家/地区 pre-fills 简体中文 and 中国 (CN). Clearing the country box and typing 「中国」 leaves `中国 (CN)` in the box; Confirm sent exactly one write, `PUT /api/settings/localization {"locale":"zh-CN","default_country":"CN"}`, answered 200, and the row turned done, progress `已完成 3/6`. `/apps/setup` afterwards: 0 dialogs. - After, en browser (`en-US`, `America/New_York`): `3 of 6 done`; opening Default currency pre-fills `USD — US Dollar` and sends no write; `/apps/setup`: 0 dialogs. </details> <details><summary>Ablations (taken on #30552, each through scripts/ablation-replace.mjs, restored to the HEAD blob with git diff HEAD empty)</summary> - `SettingsField.tsx` name match removed: `SettingsField.domainOptionTypedName-30206` 3 failed, 2 passed (the two verbatim cases stay green, as they should). - `settingsFieldRefusals.ts` re-wording disabled: 7 failed across `settingsFieldRefusals-30206`, `FirstRunChecklist` and `SettingsView.field-errors`. - `mayWrite` no longer reads the manifest's `writePermission`: `FirstRunChecklist` "a member … nothing is drawn" fails, 1 of 10. - `isKeySet` no longer reads `source`: 7 failed across `FirstRunChecklist` and `firstRunChecklistLogic`. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L9qibynkEvahBBwY1bFkmH <!-- signed: domain:ui#3 · os-tesla · session_01L9qibxW6qraU2Z3mwupUGp --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 555eb1b commit 18870c9

42 files changed

Lines changed: 2671 additions & 2023 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@object-ui/console': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
feat(console)!: an administrator's first-run checklist on Home replaces the currency and time zone dialog, and a country typed by name is saved as its code (#30206)
7+
8+
Clause-②: yes (narrowing) — the published `@object-ui/i18n` packs (all ten) drop the `console.workspaceTimezonePrompt.*` keys, which had no reader outside the removed dialog. The widening half: new console i18n keys `console.firstRunChecklist.*` and `console.settingsView.invalidFormat` / `.invalidFormatHelp` / `.invalidFormatPick` (all ten packs).
9+
Metadata-protocol: none
10+
11+
<!-- adr-0087: not-required (runtime-interface-only ui/packages/i18n/src/locales/en.ts#TranslationKeys) the key type of the console's own translation catalogue: no Zod schema, no spec declaration and no stored metadata reads it, so the compiler is the channel that reaches every caller -->
12+
13+
The dialog that asked for the system timezone and default currency opened over the first page an administrator visited, before anything needed a currency, and once covered the toast that said how to share an invitation. It is gone. The same questions, and the other things a new workspace needs, are now rows of a checklist at the top of Home (`/home`):
14+
15+
1. **Organization name**: done once the organization no longer carries the name the platform gave it ("Default Organization"). Links to Setup → Organization.
16+
2. **Default language and country**, 3. **System timezone**, 4. **Default currency**: done once the setting is set (for language, country and timezone, set by someone rather than left at the platform default; for the currency, which has no default, holding any value). Each opens in place with the Settings page's own fields, pre-filled from the browser: language, country and currency from the browser's first language (so `zh-CN` offers 中文, China and CNY together), the timezone from the browser's zone. Nothing is written until the row's Confirm, which saves through the Settings page's own `PUT /api/settings/localization`.
17+
5. **Email delivery**: done once mail can deliver (a provider other than `log`, a From address, and a host for SMTP). Shown only to a session that may write mail. Links to Setup → Mail Delivery.
18+
6. **Invite colleagues**: done once there is a second member or a pending invitation. Links to Setup → Users.
19+
20+
Each row's state is read from the real setting every time, never from a click. The checklist is shown only to a session that may write `localization`; a member sees nothing, and a member's Home sends no settings namespace read. It draws nothing once every row is done, and its close button hides it for that administrator in that workspace on that device, writing nothing to the server. The rules the dialog was built on still hold: the browser's zone and currency are never written without the administrator's Confirm, and an unset currency stays on the list until it is set.
21+
22+
**BREAKING** (`@object-ui/i18n`, TypeScript only): the exported `TranslationKeys` (`typeof en`) loses the 14 `console.workspaceTimezonePrompt.*` keys, in all ten packs (ar, de, en, es, fr, ja, ko, pt, ru, zh). A `t()` call that names one still compiles, since its key is a `string`, and reads the key back. Nothing replaces them one for one; the fix is to delete the reference.
23+
24+
| Removed key | Replacement |
25+
|:---|:---|
26+
| `console.workspaceTimezonePrompt.title`, `.description`, `.laterHint`, `.decline`, `.confirm`, `.saved`, `.notNow` | none: the dialog is retired; the timezone is a row of `console.firstRunChecklist` (`.timezone.*`) |
27+
| `console.workspaceTimezonePrompt.currencyTitle`, `.currencyDescription`, `.currencyConfirm`, `.currencySaved` | none: the currency is a row of `console.firstRunChecklist` (`.currency.*`) |
28+
| `console.workspaceTimezonePrompt.bothTitle`, `.bothConfirm`, `.bothSaved` | none: the checklist confirms each row on its own |
29+
30+
Typing a country's name into a country picker now saves its code: 「中国」 typed into 默认国家, whose option reads 「中国 (CN)」, saves `CN` instead of being refused by the server's two-letter format check. Only a name exactly one option carries counts; any other text is sent as typed, as before. When the server does refuse a value for its format (`invalid_format`), the Settings page and the checklist now say so in the reader's language, naming the setting by its label: "Choose a value for Default country from the list." for a picker, and "Gateway model is not in a valid format. Use provider/model." (with the field's own help line) for a typed field. Every other refusal keeps the server's sentence.

‎docs/qa/platform-checklist/RUNNER.md‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -204,17 +204,18 @@ contradicts it, and correct it here when it does.
204204
read-only" assumption: the pair is the door, and the read-only-package lock the
205205
console renders is a *different* gate (see `access-security.readonly-package-locks-studio`).
206206

207-
- **A fresh organization's first page opens under a one-time prompt: "Set the default
208-
currency", or the system timezone, or both in one dialog.** An administrator is asked
209-
once while `localization.timezone` is still the manifest default or the organization
210-
has no default currency (`ui/apps/console/src/pages/settings/WorkspaceTimezonePrompt.tsx`,
211-
`workspaceTimezonePromptLogic.ts`), so on a fresh DB the dialog
212-
(`data-testid="workspace-timezone-prompt"`) sits over the first page the admin opens,
213-
Studio and app pages included (QA runs #29890 and #29893). Dismiss it with **Not now**
214-
(Keep the default when only the timezone is asked), `data-testid="workspace-timezone-prompt-decline"`,
215-
before any screenshot or DOM read. The answer is remembered per organization and user in
216-
that browser's `localStorage` (`os:workspace-timezone-prompt:<org>:<user>`), so a new
217-
browser context asks again. ⛔ Do not score a clause FAIL for chrome hidden under it.
207+
- **A fresh organization's Home carries a first-run checklist for the administrator; no
208+
dialog opens over any page.** Until objectstack#30206 a one-time "Set the default
209+
currency" / system timezone dialog sat over the first page an administrator opened. It
210+
is gone: the same questions are rows of a card at the top of `/home`
211+
(`ui/apps/console/src/pages/settings/FirstRunChecklist.tsx`,
212+
`data-testid="first-run-checklist"`), shown only to a session that may write
213+
`localization`, and it writes nothing until a row's own Confirm. Studio, Setup and app
214+
pages open with nothing in front of them. A screenshot of Home on a fresh DB shows the
215+
card above the page's sections; its close button
216+
(`data-testid="first-run-checklist-dismiss"`) hides it for that organization and user in
217+
that browser's `localStorage` (`os:first-run-checklist:<org>:<user>`). ⛔ Do not score a
218+
Home clause FAIL because the card pushes the page's sections down.
218219

219220
- **`objectstack verify --rls` is a separate invocation — bare `verify` prints no RLS
220221
section at all.** `runRlsProofs` runs only behind the flag
Lines changed: 2 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { chromium, request, type FullConfig, type LaunchOptions } from '@playwright/test';
1+
import { request } from '@playwright/test';
22
import { mkdirSync, writeFileSync } from 'node:fs';
33
import { dirname } from 'node:path';
44

@@ -19,9 +19,8 @@ const API = process.env.SMOKE_API_URL || 'http://localhost:3000';
1919
const EMAIL = process.env.SMOKE_EMAIL || 'admin@objectos.ai';
2020
const PASSWORD = process.env.SMOKE_PASSWORD || 'admin123';
2121
const STATE_PATH = 'e2e/.auth/state.json';
22-
const APP = process.env.SHOWCASE_APP || 'com.example.showcase';
2322

24-
export default async function globalSetup(config: FullConfig) {
23+
export default async function globalSetup() {
2524
const ctx = await request.newContext();
2625
const res = await ctx.post(`${API}/api/v1/auth/sign-in/email`, {
2726
data: { email: EMAIL, password: PASSWORD },
@@ -38,102 +37,5 @@ export default async function globalSetup(config: FullConfig) {
3837
};
3938
mkdirSync(dirname(STATE_PATH), { recursive: true });
4039
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
41-
42-
// The specs' own launch options (playwright.config.ts carries
43-
// `OS_TEST_CHROMIUM_EXECUTABLE_PATH` there), so the prompt is answered in the
44-
// browser the specs run in.
45-
const promptRecord = await declineWorkspacePrompt(token, config.projects[0]?.use?.launchOptions);
46-
if (promptRecord.length > 0) {
47-
state.origins[0].localStorage.push(...promptRecord);
48-
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
49-
}
5040
}
5141

52-
/**
53-
* Decline the console's one-time workspace prompt before any spec runs.
54-
*
55-
* The console asks an administrator, in ONE modal dialog on the first app page
56-
* it renders, to set the workspace timezone while it is still the manifest
57-
* default, and — since the prompt's currency arm (objectstack#28872, commit
58-
* 2ac698baf1) — the default currency while `localization.currency` has no
59-
* value. The showcase's dev seed derives the currency from the host's POSIX
60-
* locale and leaves it unset on a host that sets none or names no country, and
61-
* the seeded admin may write settings, so every spec would meet that dialog in
62-
* front of the page it reads: the modal makes the page behind it
63-
* `aria-hidden`, so role queries (`getByRole('checkbox')`, `getByRole('table')`,
64-
* a section's disclosure button) find nothing and a click never lands.
65-
*
66-
* The run answers it ONCE, in a real browser, through the prompt's own decline
67-
* button ("Not now" / "Keep the default"), the same way the console's own live
68-
* suite does (ui/e2e/live/global-setup.ts). Declining writes nothing to the
69-
* workspace — the showcase stays exactly as seeded, so no spec's reading of an
70-
* amount moves — and the prompt keeps its per-device record of having asked in
71-
* this origin's localStorage (`PROMPT_RECORD_PREFIX`:ORG:USER). That record,
72-
* and nothing else the console stored while it was open, is what this returns
73-
* for the storageState every spec reuses: the rest (the sidebar's wide-open
74-
* flag, recent items, the active organization) would change the fresh-context
75-
* layout the specs measure from — detail-shapes derives its 780 px window from
76-
* a context with no stored sidebar state.
77-
*
78-
* Skipped when the backend says the prompt would not ask (the console's own
79-
* condition per key, `workspaceTimezonePromptLogic.ts`). A prompt that was
80-
* expected but never shown is reported, not failed: the specs then show whether
81-
* anything stands in their way. A prompt that was declined but left no record
82-
* throws: every spec would meet it again, and that is the run's one cause.
83-
*/
84-
const PROMPT_RECORD_PREFIX = 'os:workspace-timezone-prompt:';
85-
86-
async function declineWorkspacePrompt(
87-
token: string,
88-
launchOptions: LaunchOptions | undefined,
89-
): Promise<{ name: string; value: string }[]> {
90-
const api = await request.newContext({ extraHTTPHeaders: { Authorization: `Bearer ${token}` } });
91-
let wouldAsk = false;
92-
try {
93-
const res = await api.get(`${API}/api/settings/localization`);
94-
if (res.ok()) {
95-
const body = await res.json();
96-
const values = (body?.data ?? body)?.values;
97-
const timezone = values?.timezone;
98-
const currency = values?.currency;
99-
const timezoneUnset = timezone?.source === 'default' && !timezone?.locked;
100-
const currencyUnset =
101-
currency?.source === 'default' &&
102-
!currency?.locked &&
103-
(currency?.value === null || currency?.value === undefined || currency?.value === '');
104-
wouldAsk = timezoneUnset || currencyUnset;
105-
}
106-
} finally {
107-
await api.dispose();
108-
}
109-
if (!wouldAsk) return [];
110-
111-
const browser = await chromium.launch(launchOptions);
112-
try {
113-
const context = await browser.newContext({ storageState: STATE_PATH, baseURL: API });
114-
const page = await context.newPage();
115-
await page.goto(`/_console/apps/${APP}`, { waitUntil: 'domcontentloaded' });
116-
const prompt = page.getByTestId('workspace-timezone-prompt');
117-
try {
118-
await prompt.waitFor({ state: 'visible', timeout: 60_000 });
119-
} catch {
120-
console.warn('[showcase-smoke] the workspace timezone or currency is unset, but the console showed no prompt');
121-
return [];
122-
}
123-
await page.getByTestId('workspace-timezone-prompt-decline').click();
124-
await prompt.waitFor({ state: 'hidden' });
125-
const stored = await context.storageState();
126-
const record = (stored.origins.find((o) => o.origin === new URL(API).origin)?.localStorage ?? []).filter((e) =>
127-
e.name.startsWith(PROMPT_RECORD_PREFIX),
128-
);
129-
if (record.length === 0) {
130-
throw new Error(
131-
`the workspace prompt was declined but left no ${PROMPT_RECORD_PREFIX}* record in localStorage, ` +
132-
'so every spec would meet it again',
133-
);
134-
}
135-
return record;
136-
} finally {
137-
await browser.close();
138-
}
139-
}

‎ui/apps/console/src/App.tsx‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
} from '@object-ui/app-shell';
4343

4444
import { AppContent, ConsolePermissionsRequest, ConsolePermissionsScope } from './AppContent';
45+
import { FirstRunChecklist } from './pages/settings/FirstRunChecklist';
4546
import { publishAuthState } from './i18nSession';
4647
import { FaviconSync } from './components/FaviconSync';
4748
import { RootLandingRedirect } from './components/RootLandingRedirect';
@@ -195,11 +196,18 @@ function NativeShellRouting() {
195196
return null;
196197
}
197198

198-
/** Wraps `DefaultHomeLayout` so the FAB gets the signed-in user id. */
199+
/**
200+
* Wraps `DefaultHomeLayout` so the FAB gets the signed-in user id, and draws
201+
* an administrator's first-run checklist above the page's own sections
202+
* (objectstack#30206). The route sits under `ConsolePermissionsScope`, which
203+
* is what tells the checklist whether this session may write the settings it
204+
* lists; for anyone else it draws nothing.
205+
*/
199206
function HomeRoute() {
200207
const { user } = useAuth();
201208
return (
202209
<DefaultHomeLayout userId={user?.id}>
210+
<FirstRunChecklist />
203211
<DefaultHomePage />
204212
</DefaultHomeLayout>
205213
);

‎ui/apps/console/src/AppContent.tsx‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { createAuthenticatedFetch, useAuth } from '@object-ui/auth';
1818
import type { DataSource } from '@object-ui/types';
1919
import { LocalizationFetchProvider } from './LocalizationFetchProvider';
2020
import { InAppFormRoute } from './components/InternalFormRoute';
21-
import { WorkspaceTimezonePrompt } from './pages/settings/WorkspaceTimezonePrompt';
2221
import { APPROVAL_ACTION_OBJECT, APPROVAL_REQUEST_OBJECT, routeApprovalRecordReads } from './services/approvalRequestsDataSource';
2322

2423
const AppManagementPage = lazy(() => import('./pages/system/AppManagementPage').then(m => ({ default: m.AppManagementPage })));
@@ -528,11 +527,6 @@ export function AppContent() {
528527
/>
529528
</ApprovalRecordReads>
530529
</LocalizationFetchProvider>
531-
{/* objectui#11758 — asks an administrator once to set a still-default
532-
workspace timezone. Under `ConsolePermissionsScope`, because that
533-
provider is what answers whether this session may write the
534-
setting; mounted once for the whole app shell, beside the routes. */}
535-
<WorkspaceTimezonePrompt />
536530
</>
537531
);
538532
}

‎ui/apps/console/src/__tests__/App.homePermissions-12700.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ vi.mock('../../../../packages/app-shell/src/hooks/useAiSurface', async (importOr
178178
vi.mock('../LocalizationFetchProvider', () => ({
179179
LocalizationFetchProvider: ({ children }: { children?: ReactNode }) => <>{children}</>,
180180
}));
181-
vi.mock('../pages/settings/WorkspaceTimezonePrompt', () => ({ WorkspaceTimezonePrompt: () => null }));
181+
vi.mock('../pages/settings/FirstRunChecklist', () => ({ FirstRunChecklist: () => null }));
182182
vi.mock('../components/RootLandingRedirect', () => ({ RootLandingRedirect: () => null }));
183183
vi.mock('../components/SetupRoute', () => ({ SetupRoute: () => null }));
184184
vi.mock('../components/FormPage', () => ({ FormPage: () => null }));

‎ui/apps/console/src/__tests__/App.permissionsWave-29114.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ vi.mock('@object-ui/app-shell', async (importOriginal) => {
145145
vi.mock('../LocalizationFetchProvider', () => ({
146146
LocalizationFetchProvider: ({ children }: { children?: ReactNode }) => <>{children}</>,
147147
}));
148-
vi.mock('../pages/settings/WorkspaceTimezonePrompt', () => ({ WorkspaceTimezonePrompt: () => null }));
148+
vi.mock('../pages/settings/FirstRunChecklist', () => ({ FirstRunChecklist: () => null }));
149149
vi.mock('../components/RootLandingRedirect', () => ({ RootLandingRedirect: () => null }));
150150
vi.mock('../components/SetupRoute', () => ({ SetupRoute: () => null }));
151151
vi.mock('../components/FormPage', () => ({ FormPage: () => null }));

‎ui/apps/console/src/__tests__/AppContent.approvalRecordReads.test.tsx‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@ vi.mock('@object-ui/permissions', async (importOriginal) => ({
3636
vi.mock('../LocalizationFetchProvider', () => ({
3737
LocalizationFetchProvider: ({ children }: { children: ReactNode }) => <>{children}</>,
3838
}));
39-
vi.mock('../pages/settings/WorkspaceTimezonePrompt', () => ({
40-
WorkspaceTimezonePrompt: () => null,
41-
}));
4239

4340
import { AppContent } from '../AppContent';
4441

‎ui/apps/console/src/components/FormPage.routePermissions-29187.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ vi.mock('../../../../packages/app-shell/src/hooks/useAiSurface', async (importOr
137137
vi.mock('../LocalizationFetchProvider', () => ({
138138
LocalizationFetchProvider: ({ children }: { children?: ReactNode }) => <>{children}</>,
139139
}));
140-
vi.mock('../pages/settings/WorkspaceTimezonePrompt', () => ({ WorkspaceTimezonePrompt: () => null }));
140+
vi.mock('../pages/settings/FirstRunChecklist', () => ({ FirstRunChecklist: () => null }));
141141
vi.mock('../components/RootLandingRedirect', () => ({ RootLandingRedirect: () => null }));
142142
vi.mock('../components/SetupRoute', () => ({ SetupRoute: () => null }));
143143
vi.mock('../components/FormPage', () => ({ FormPage: () => <GrantProbe testId="public-form" /> }));

‎ui/apps/console/src/loadLocales.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ let bootPainted: Promise<void> | null = null;
4040
* page load, so every reader that waits on it is released by the same paint.
4141
*
4242
* Shared by the console's two readers whose answer no first-paint surface
43-
* needs, this one and `WorkspaceTimezonePrompt`'s settings read. It waits on
44-
* the splash's DOM marker rather than on any one component, because both are
45-
* mounted beside the routes, under the splash. A tab opened in the background
43+
* needs, this one and `FirstRunChecklist`'s settings read. It waits on the
44+
* splash's DOM marker rather than on any one component, because neither is
45+
* the surface the splash waits for. A tab opened in the background
4646
* paints no frame, so it resolves when the tab is first shown.
4747
*/
4848
export function whenBootPainted(): Promise<void> {

0 commit comments

Comments
 (0)