Skip to content

fix(metadata-admin): Studio authoring UX — typeable identifiers + 5 dogfood fixes - #1926

Merged
os-zhuang merged 2 commits into
mainfrom
fix/identifier-input-underscore
Jun 23, 2026
Merged

fix(metadata-admin): Studio authoring UX — typeable identifiers + 5 dogfood fixes#1926
os-zhuang merged 2 commits into
mainfrom
fix/identifier-input-underscore

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Summary

Six fixes for the Studio metadata designer, found by dogfooding the console as a business user (building an object end-to-end, then exercising it at runtime). The platform's runtime data layer proved solid (CRUD + required/picklist/number/date validation all enforced server-side); every issue was in the authoring UX.

Each fix was verified in the browser against a live backend via the dev console (pnpm dev → Vite on :5181, /api proxied to a real :3000 instance).

#ProblemFix
F2Object Name / field API-name silently drop _ while typing (repair_ticketrepairticket); non-Latin labels collapse to the literal field. Root cause: InspectorTextField is a controlled input that re-runs the strict toFieldName on every keystroke, and its trailing-_ trim eats the underscore the instant it's typed.Add prefix-stable toFieldNameLoose (keeps a trailing _, returns '' not field on empty) for live-typing inputs; keep strict toFieldName for complete-string callers (group keys); switch label→name derivation to slugify (its documented "empty for non-Latin → prompt" contract).
F1A newly created package doesn't appear in the active-package switcher until a full page reload.useSelectorOptions refetches on dropdown-open and on a global objectui:packages-changed event, which PackagesPage dispatches after a successful create.
F3A field's API-name never derives from its label (only the object Name does).Optional onBlur on InspectorTextField; the field inspector derives the API-name from the label on blur — only while the name is still an auto-generated default (text, text_2, …), so manual names are never clobbered.
F4Adding a picklist / clicking "Add value" creates an empty option row that immediately fails spec validation with a developer-oriented "System identifier must be at least 2 characters".newField seeds no options; OptionsEditor keeps blank rows in a local buffer and persists only rows whose value is non-empty.
F5The Source tab renders a permanent blank when Monaco's (CDN-loaded) core or web workers are unavailable — offline / air-gapped / CSP-restricted installs.JsonSourceEditor detects that nothing painted (.view-line) and falls back to a plain, editable textarea showing the JSON source.
F6A user's own object, after publishing to a project package, shows "This item is shipped by a code package and cannot be modified at runtime" — misleading, with no stated edit path.Reworded artifactLockedBanner (en + zh) to "provided by an installed package … edit it in its source package and republish — or create a new {type} from scratch" — accurate for both platform and project packages, and states the real edit path. (The read-only-after-publish behavior itself is intentional governance and is unchanged.)

Verification

  • Unit:object-fields-io.test.ts — 32 pass, incl. a controlled-input typing simulation that reproduces the old repairticket regression and proves the fix yields repair_ticket.
  • In-browser (live backend): F1 (new package appears without reload), F2 (work_order typed keystroke-by-keystroke through the real onChange pipeline), F3 (Asset Codeasset_code on blur), F4 (no validation banner on empty rows), F5 (textarea fallback shows the draft JSON), F6 (accurate banner on a published object).
  • Changed files add zero new lint errors/warnings.

Notes

  • Console UI only; no schema/runtime changes.
  • F5 also improves the offline/air-gapped story (Monaco core is CDN-loaded by default with no fallback today).

🤖 Generated with Claude Code

os-zhuangand others added 2 commits June 24, 2026 00:39
The object designer's Name field and every field's API-name input are
controlled inputs whose onChange re-normalizes on every keystroke via the
strict `toFieldName`, which trims a trailing "_". Typing "repair_ticket"
therefore produced "repairticket": the moment "_" is typed the value is
"repair_" → trimmed to "repair" → the underscore is gone before the next
letter arrives. Non-Latin (e.g. Chinese) authors hit this hardest — their
label can't derive a Latin slug, so they MUST type the identifier by hand,
and could only ever produce single-token names.
Add a prefix-stable `toFieldNameLoose` for live keystroke input: it keeps a
single trailing "_" (itself a spec-valid identifier char) so typing can
continue, and returns "" instead of the "field" placeholder on empty/CJK
input so clearing the box actually clears it. Wire the object Name and
field API-name inputs to it. Keep strict `toFieldName` for complete-string
callers (group-key generation). Switch the label→name derivation to
`slugify` (its documented contract: empty for non-Latin → prompt for
manual entry) instead of silently seeding "field".
Tests: 32 pass, incl. a controlled-input typing simulation that reproduces
the old "repairticket" regression and proves the fix yields "repair_ticket".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
derive, picklist rows, source fallback, read-only wording
Five dogfood-found Studio fixes (companion to the identifier-input fix):
F1 — Package switcher didn't show a newly created package until a full
page reload. `useSelectorOptions` now refetches on dropdown-open and on a
global `objectui:packages-changed` event, which `PackagesPage` dispatches
after a successful create.
F3 — A field's API name never derived from its label (only the object
Name did). Added an optional `onBlur` to `InspectorTextField`; the field
inspector derives the API name from the label on blur — but only while the
name is still an auto-generated default (e.g. `text`, `text_2`), so manual
names are never clobbered. Uses the trailing-trimming `slugify` (correct
for a complete string) and stays empty for non-Latin labels.
F4 — Adding a picklist (or clicking "Add value") created an empty option
row that immediately failed spec validation with a developer-oriented
"System identifier must be at least 2 characters" error. `newField` now
seeds no options, and `OptionsEditor` keeps blank rows in a local buffer,
persisting only rows whose value is non-empty.
F5 — The Source tab rendered a permanent blank when Monaco's (CDN-loaded)
core or its web workers were unavailable — offline / air-gapped / CSP
installs. `JsonSourceEditor` now detects that nothing painted and falls
back to a plain, editable textarea showing the JSON source.
F6 — A user's own object, after publishing it to a project package, showed
"This item is shipped by a code package and cannot be modified at runtime",
which is misleading. Reworded `artifactLockedBanner` (en + zh) to "provided
by an installed package … edit it in its source package and republish — or
create a new {type} from scratch", which is accurate for both platform and
project packages and states the actual edit path.
All five verified in-browser against a live backend via the dev console.
object-fields-io unit tests: 32 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercelBot commented Jun 23, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredJun 23, 2026 5:48pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)58.8 KB350 KB
Entry fileindex-CNmIrEcF.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)6.59KB2.34KB
app-shell (runtime-config.js)4.72KB1.69KB
app-shell (types.js)0.01KB0.04KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)17.16KB3.44KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)4.79KB1.88KB
auth (LoginForm.js)5.11KB1.92KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.63KB2.15KB
auth (SocialSignInButtons.js)8.89KB3.61KB
auth (UserMenu.js)3.40KB1.22KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)24.74KB6.00KB
auth (createAuthenticatedFetch.js)3.33KB1.32KB
auth (index.js)1.75KB0.76KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.01KB0.79KB
auth (useIsWorkspaceAdmin.js)1.28KB0.70KB
collaboration (CommentThread.js)18.38KB4.49KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)3.65KB1.42KB
collaboration (PresenceProvider.js)2.42KB0.96KB
collaboration (index.js)1.25KB0.53KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)415.39KB88.42KB
core (index.js)1.52KB0.55KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)97.73KB24.27KB
fields (index.js)166.21KB40.08KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.46KB0.96KB
i18n (pickLocalized.js)1.31KB0.67KB
i18n (provider.js)5.37KB1.72KB
i18n (useObjectLabel.js)21.15KB4.68KB
i18n (useSafeTranslation.js)2.68KB0.98KB
layout (index.js)36.30KB10.04KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)4.42KB1.27KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)1.77KB0.77KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)5.09KB1.84KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.46KB1.03KB
permissions (evaluator.js)4.00KB1.23KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.50KB0.70KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.98KB12.65KB
plugin-charts (index.js)43.72KB12.23KB
plugin-chatbot (index.js)153.41KB37.19KB
plugin-dashboard (index.js)106.56KB26.34KB
plugin-designer (index.js)213.42KB42.94KB
plugin-detail (index.js)195.59KB47.19KB
plugin-editor (index.js)2.38KB1.06KB
plugin-form (index.js)93.24KB22.71KB
plugin-gantt (index.js)133.11KB32.92KB
plugin-grid (index.js)115.41KB31.01KB
plugin-kanban (index.js)48.82KB13.13KB
plugin-list (index.js)94.53KB22.32KB
plugin-map (index.js)16.02KB4.98KB
plugin-markdown (index.js)13.58KB4.64KB
plugin-report (index.js)35.93KB9.50KB
plugin-timeline (index.js)25.37KB7.20KB
plugin-tree (index.js)8.04KB2.70KB
plugin-view (index.js)84.45KB20.36KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.55KB0.67KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)17.06KB5.59KB
react (index.js)0.76KB0.42KB
tenant (TenantContext.js)0.31KB0.25KB
tenant (TenantGuard.js)1.04KB0.43KB
tenant (TenantProvider.js)2.76KB0.98KB
tenant (TenantScopedQuery.js)0.77KB0.44KB
tenant (index.js)0.75KB0.38KB
tenant (resolver.js)2.64KB0.76KB
tenant (useTenant.js)0.50KB0.32KB
tenant (useTenantBranding.js)0.62KB0.39KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)0.77KB0.41KB
types (disclosure.js)0.20KB0.18KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (index.js)1.54KB0.68KB
types (layout.js)0.20KB0.18KB
types (mobile.js)0.20KB0.18KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.26KB1.96KB
types (tenant.js)0.20KB0.18KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)0.75KB0.46KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-zhuang
os-zhuang merged commit 9ccec47 into mainJun 23, 2026
10 checks passed
@os-zhuang
os-zhuang deleted the fix/identifier-input-underscore branch June 23, 2026 17:56
os-zhuang added a commit that referenced this pull request Jun 24, 2026
…estore F6 wording (#1941)
* test(metadata-admin): add data-testid hooks to the Studio object designer
Stable selectors for the live-e2e/dogfood gate (no behavior change):
object-name-input, field-apiname-input, field-label-input (via an optional
InspectorTextField testId prop), package-id-input/package-name-input,
package-switcher, source-editor, metadata-validation-banner, readonly-banner.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(metadata-admin): Studio object-designer dogfood gate (F1-F6)
A "drive the Studio designer as a user" regression gate for the UX classes
fixed in #1926 — the kind static gates (build/unit/spec-liveness) miss because
the break only appears when a real user drives the real controlled inputs
against the real spec validator.
Live e2e (e2e/live/studio-object-designer.spec.ts, runs against the real stack):
F2 — object Name accepts a TYPED underscore. Must pressSequentially, not
fill(): .fill() sets the value in one shot and hides the per-keystroke
trim. Proven to go red when the trailing-trim bug is reintroduced.
F3 — a field API name derives from its label on blur.
F4 — adding a picklist + an empty option row does NOT trip spec validation.
Live validation is debounced, so the negative assertion settles first;
proven red when empty rows are persisted again.
F1 — a newly created package appears in the switcher without a reload
(creates + cleans up a throwaway package).
Component/unit (fast layer, no backend):
F5 — JsonSourceEditor falls back to an editable textarea when Monaco can't
paint (offline/CSP/worker-blocked). fallbackDelayMs added so the test
doesn't wait the full grace period.
F6 — artifactLockedBanner wording (en + zh): not "shipped by a code
package", and states the real edit path.
This gate immediately earned its keep: the F6 test failed on first run because
a parallel PR (#1937, branched off pre-#1926 main) had silently reverted the
banner wording. Re-applied here (i18n.ts) — exactly the regression class the
gate exists to catch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-zhuang