Repository navigation
feat(platform-objects,service-messaging): Setup lists show their own empty state; API Keys gains Create Key - #23924
Conversation
…wn empty states; API Keys gains Create Key WIP: object metadata only; bundles, tests and changeset follow. Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi Co-authored-by: Claude <noreply@anthropic.com>
…ocale bundle; metadata tests; changeset Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6ba79fd0acb60e24871c64d9e7346a947feb2485 && git checkout 6ba79fd0acb60e24871c64d9e7346a947feb2485
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin abb6df0e0109f32c8a51f94a4deaaa4518a0b5a2 8944600e58305440f80d0448bb629ba558fc3847 && git checkout -B drift-repro abb6df0e0109f32c8a51f94a4deaaa4518a0b5a2 && git merge --no-ff 8944600e58305440f80d0448bb629ba558fc3847
node scripts/docs-audit/affected-docs.mjs --json abb6df0e0109f32c8a51f94a4deaaa4518a0b5a2
|
席位核对 · ✅ ACCEPT: PR #23924 at
|
Fixes #23852
Clause-②: no
Setup's Invitations, API Keys, OAuth Applications and the three notification lists now draw their own empty state instead of the console's fallbacks, and API Keys gains a Create Key button that mints through the existing
POST /api/v1/keysroute. Metadata, bundles and tests only; no field, index, permission or route changes.What changes
1. Every list view declares its own
emptyState(title + one sentence).sys_invitationpending(Setup opens this one)acceptedexpiredall_invitationssys_api_keyall_keys(Setup)mine(Account app)activerevokedsys_oauth_applicationall_apps(Setup)mine(Account app)activedisabled_appssys_notification_preferenceall(new)sys_notification_subscriptionall(new)sys_notification_templateall(new)The three identity objects are
managedBy: 'better-auth', so with no declaration objectui drew the identity fallback ("created by the authentication provider … not added by hand here"), which is false for all three.The three notification objects had no list view at all (a PM mechanism assumption this falsifies: the claim says "the list views of" them, and there were none). The only per-list empty copy the spec carries is a list view's
emptyState, so each now ships one view,all, declared for that copy alone. It hascolumns: [], which is the spec's own "declares no projection, left to the renderer" form, so objectui keeps deriving the same columns fromhighlightFields(org attribution column included), and it has nofilter,sortorpagination. The view idallis the id objectui's synthesized tab already had. The one visible side effect is the tab label: "All" (translated) instead of the console's "All records".2. API Keys:
create_api_key("Create Key"), a primarylist_toolbaraction. It callsPOST /api/v1/keyswith a requiredname(field-backed onsys_api_key.name) and shows the raw key in aresultDialog(path: 'key',format: 'secret'). It declares nosuccessMessage, because the dialog suppresses the toast. It appears on everysys_api_keylist, Setup'sall_keysand the Account app'smine. The route pins the key to the caller (user_idand the active organization are never read from the body), so an admin on Setup mints their own key exactly as a user does on My Keys. The param dialog'sdescriptionsays this.3. Copy in every locale. The
enbundles are regenerated withnode scripts/check-i18n-bundles.mjs --write, and thezh-CN,ja-JPandes-ESleaves are hand-written. Rerunning--writeafter the translation dropped every provenance row the fill had added, so the*.source-hashes.generated.tsfiles have no net change.Item 2's route reading (the card's condition)
The card asks the claimant to confirm the route and, if a new door is needed, to treat that half as its own question. No new door is needed:
POST /api/v1/keys:packages/runtime/src/domains/keys.ts(createKeysDomains/handleKeysRequest), mounted atserver.post(prefix + '/keys')inpackages/runtime/src/dispatcher-plugin.ts, route-ledger rowPOST /keys(disposition: 'sdk',client: 'keys.create'). It is the only mint path. It whitelistsnameand an optionalexpires_at, answers201 { success, data: { id, name, prefix, key, … } }with the raw key once, and stores only the hash. It does not depend onsys_api_key'screateaffordance, which stays off. The PM's grep forcreateApiKey/api-keysmissed it because the names arekeys/createKeysDomains.origin/maina5bb7ac) are themcp:connect-agentwidget (packages/app-shell/src/console/connect/ConnectAgentWidget.tsx) and the developer Integrations page (apps/console/src/pages/developer/AgentConnectSection.tsx). The Account app's API Keys entry (nav_account_api_keys→sys_api_keyviewmine) rendered the same object list as Setup, with no create entry. This action is now that entry on both pages.apiHandler(useConsoleActionRuntime.tsx) POSTs an absolutetype: 'api'target with the session cookie, unwraps{ success, data }, and resolvesresultDialogpaths againstdata. That is the mechanismsys_oauth_application.create_oauth_applicationalready uses for its one-shot client secret.Tests (metadata level)
packages/platform-objects/src/identity/setup-list-empty-states.test.ts(new). For the three identity objects, the population is each object's declared views. It checks that every view declares a non-empty title and message, that no two views of one object share a title (a filtered view names itself), and that Pending's title is "No pending invitations" (the card's acceptance names it). It checks that theenbundle carries each view's copy as declared and thatzh-CN/ja-JP/es-EScarry it. For Create Key, it checks the primarylist_toolbarplacement,POST /api/v1/keys, thekeyreveal assecret, params limited to what the route reads and backed by real fields,createstill absent fromapiMethods, and that the two empty states naming Create Key carry the action's label.packages/platform-objects/src/identity/two-factor-one-shot-reveal.test.ts: the class guard'sONE_SHOT_ROUTEStable gains/api/v1/keys→['key']. Every action on that route is now held to a coveringresultDialogand nosuccessMessage, the same as the 2FA routes. This closes the class rather than adding a single-point pin.packages/services/service-messaging/src/objects/setup-list-empty-states.test.ts(new): every view of the three notification objects declares the copy, theallview declares no projection, filter, sort or page size, and all four bundles carry the label and copy.Run on
8944600e5(the PR head, after mergingorigin/main):pnpm --filter @objectstack/platform-objects test: 72 files / 1128 tests passed.pnpm --filter @objectstack/service-messaging test: 52 files / 584 tests passed.pnpm --filter @objectstack/platform-objects --filter @objectstack/service-messaging typecheck: exit 0.tsc --listFilesconfirms all three test files are in a type-check program.Ablation, one per new pin, each via
scripts/ablation-replace.mjs(the anchor hit once, the blob changed, and the restore was proven by blob == HEAD and an emptygit diff HEAD). The subjects are imported fromsrc, so no build ordistleg applies:titlemade 3 tests red: every-view-declares, the Pending pin and the en-bundle equality.{ path: 'key' … }reveal field made 2 tests red: the one-shot class guard ("does not reveal 'key'") and the masked-reveal pin.sys_notification_template'scolumns: []to['topic', 'channel']made the no-projection pin red.Gates
The derived list is
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat8944600e5. The run is reconciled with--ran(exit codes recorded): 65 derived, 64 run with exit 0, 1 NOT MEASURED, 0 unrun.pnpm check:dual-build-cjs-loadsexited 3 with PREREQUISITE NOT MET, because it reads every package'sdist/and this worktree built only the i18n closure. CI'sLint & Repo Gatesruns it.pnpm check:i18nandpnpm check:i18n-coverageexit 0. The coverage log reads "14 config(s), 621 baselined untranslated string(s), none new".node scripts/check-changeset-fixed.mjs,pnpm check:authz-resolver,pnpm check:error-code-casingandpnpm check:filter-alias-parity.Acceptance notes (observed, not filed)
sys_oauth_application'sactiveview filtersdisabled == false. The object's comments say better-auth writesdisabledonly when it is set. If such rows hold NULL, theequals falsefilter would leave never-toggled applications out of Active. This is not measured here and is an inference from the comment alone. The Active empty state is worded so it stays true either way ("stays active until it is disabled").sys_api_key's header docblock says the raw token is returned "via the auth plugin API". The mint path is the runtimekeysdomain. This is comment drift and was left untouched.service-messaging'sja-JP/es-ESbundles are otherwise English for these three objects (pre-existing echoes). The new empty-state leaves are translated, so those pages mix languages until the rest is translated.examplesandpackages/qa/dogfoodwere not run locally. Thesys_api_keydogfood pins (api-key-revoke-lifecycle,api-key-owner-revoke) assert the revoke and restore actions by name, and those actions are unchanged.Generated by Claude Code