diff --git a/.changeset/studio-wizard-and-p3-polish.md b/.changeset/studio-wizard-and-p3-polish.md new file mode 100644 index 000000000..5dfd91457 --- /dev/null +++ b/.changeset/studio-wizard-and-p3-polish.md @@ -0,0 +1,29 @@ +--- +"@object-ui/app-shell": minor +"@object-ui/plugin-view": patch +--- + +Studio package-create dogfood follow-ups (framework#2615 — P2 wizard + P3 polish): + +- **Package-id wizard feedback.** The three package wizards (switcher create, + landing create, landing duplicate) share a new `PackageIdInput`: illegal + characters are still normalized away, but no longer silently — a notice + says what was removed — a reverse-domain format hint shows while the id + doesn't parse, and a CJK-only name that yields no id suggestion is told to + type one manually instead of leaving the id box mysteriously empty. +- **Records-grid duplicate "Actions" column.** A field literally named + `actions` is now dropped from the Studio grid's data columns, so it no + longer collides with the always-pinned row-actions column (it stays + editable in the form designer). +- **Record-create verb consistency.** The `ObjectView` toolbar create button + resolved a hardcoded English "Create"; it now uses the same + `console.objectView.new` ("New" / 新建) key as the runtime object pages so + Studio and the running app agree. +- **Branded cold-load splash.** The console's pre-auth loading gate rendered a + bare "Loading…"; it now shows the branded, boot-safe `LoadingScreen`. +- **Picklist option editor.** Value/label inputs and CJK option labels no + longer truncate — the six controls that shared one cramped row are split + into a two-row layout so the inputs get the full panel width. +- **Draft-save confirmation.** The Data pillar's "Save draft" now shows a + success toast and a "last saved HH:MM" indicator, matching the App and + Automations pillars. diff --git a/packages/app-shell/src/console/ConsoleShell.tsx b/packages/app-shell/src/console/ConsoleShell.tsx index 58649b2f3..8bc782e6b 100644 --- a/packages/app-shell/src/console/ConsoleShell.tsx +++ b/packages/app-shell/src/console/ConsoleShell.tsx @@ -29,14 +29,17 @@ import { useAttachUserStateAdapters, } from '../context/UserStateAdapters'; import { ThemeProvider } from '../chrome/ThemeProvider'; +import { LoadingScreen } from '../chrome/LoadingScreen'; import { RemediationOverlay } from './RemediationOverlay'; +// The console's every pre-React / pre-auth gate (Suspense fallback, adapter +// not ready, org/auth loading) renders this. It used to be a bare, unbranded +// "Loading…" line — ~8s of blank chrome before the login redirect on a cold +// /_console load (framework#2615 P3). Delegate to the branded, boot-safe +// splash (logo + product name + step list) that the rest of the shell already +// uses, so the cold load looks like the product from the first frame. export function LoadingFallback() { - return ( -
- Loading… -
- ); + return ; } /** diff --git a/packages/app-shell/src/views/metadata-admin/i18n.ts b/packages/app-shell/src/views/metadata-admin/i18n.ts index eb2a74ece..5593d6795 100644 --- a/packages/app-shell/src/views/metadata-admin/i18n.ts +++ b/packages/app-shell/src/views/metadata-admin/i18n.ts @@ -856,6 +856,8 @@ const ENGINE_STRINGS_EN: Record = { 'engine.studio.create': 'Create', 'engine.studio.createDraft': 'Create (save as draft)', 'engine.studio.saveDraft': 'Save draft', + 'engine.studio.data.savedDraft': 'Object “{label}” saved as draft', + 'engine.studio.data.lastSaved': 'Saved {time}', 'engine.studio.publish': 'Publish', 'engine.studio.loading': 'Loading…', 'engine.studio.loadFailed': 'Failed to load', @@ -954,6 +956,9 @@ const ENGINE_STRINGS_EN: Record = { 'engine.studio.pkg.none': 'No app packages yet', 'engine.studio.pkg.namePlaceholder': 'Name (e.g. Repair Center)', 'engine.studio.pkg.idPlaceholder': 'Package ID (e.g. com.example.repairs)', + 'engine.studio.pkg.idStrippedNotice': 'Unsupported characters were removed — allowed: lowercase letters, digits, dots, hyphens, underscores', + 'engine.studio.pkg.idFormatHint': 'Use a reverse-domain identifier with at least one dot, e.g. com.example.myapp', + 'engine.studio.pkg.idFromNameUnavailable': 'No identifier could be suggested from this name — type one below (e.g. com.example.myapp)', 'engine.studio.pkg.createWritable': 'Create writable package', 'engine.studio.pkg.new': 'New package (writable base)', 'engine.studio.pkg.created': 'Package {name} created (writable)', @@ -1789,6 +1794,8 @@ const ENGINE_STRINGS_ZH: Record = { 'engine.studio.create': '创建', 'engine.studio.createDraft': '创建(存为草稿)', 'engine.studio.saveDraft': '保存草稿', + 'engine.studio.data.savedDraft': '对象「{label}」已存为草稿', + 'engine.studio.data.lastSaved': '已于 {time} 保存', 'engine.studio.publish': '发布', 'engine.studio.loading': '加载中…', 'engine.studio.loadFailed': '加载失败', @@ -1887,6 +1894,9 @@ const ENGINE_STRINGS_ZH: Record = { 'engine.studio.pkg.none': '暂无应用软件包', 'engine.studio.pkg.namePlaceholder': '名称(如:维修中心)', 'engine.studio.pkg.idPlaceholder': '包 ID(如:com.example.repairs)', + 'engine.studio.pkg.idStrippedNotice': '已移除不支持的字符——仅允许小写字母、数字、点、连字符和下划线', + 'engine.studio.pkg.idFormatHint': '请使用带至少一个点的反向域名标识,如 com.example.myapp', + 'engine.studio.pkg.idFromNameUnavailable': '无法从该名称生成标识,请在下方手动输入(如 com.example.myapp)', 'engine.studio.pkg.createWritable': '创建可写软件包', 'engine.studio.pkg.new': '新建软件包(可写 base)', 'engine.studio.pkg.created': '软件包 {name} 已创建(可写)', diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx index bd47680bb..86ec1fa30 100644 --- a/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx +++ b/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx @@ -757,61 +757,72 @@ function OptionsEditor({ {rows.length === 0 ? (
{t('designer.field.noValues', locale)}
) : ( -
+
{rows.map((o, i) => ( -
- update(i, { value: e.target.value })} - placeholder={t('designer.field.optValue', locale)} - disabled={disabled} - className="h-7 text-xs font-mono flex-1" - /> - update(i, { label: e.target.value })} - placeholder={t('designer.field.optLabel', locale)} - disabled={disabled} - className="h-7 text-xs flex-1" - /> - update(i, { color: e.target.value })} - disabled={disabled} - className="h-7 w-7 rounded border bg-background cursor-pointer p-0.5" - title={t('designer.field.optColor', locale)} - /> - - - + // Two rows per option: the value/label inputs get the full panel + // width (min-w-0 lets them shrink cleanly instead of clipping their + // own placeholders), while the color swatch and reorder/remove + // controls sit on a compact strip below — previously all six + // controls shared one line, squeezing the inputs until "Value" / + // "Label" and CJK option labels truncated (framework#2615 P3). +
+
+ update(i, { value: e.target.value })} + placeholder={t('designer.field.optValue', locale)} + disabled={disabled} + className="h-7 min-w-0 flex-1 text-xs font-mono" + /> + update(i, { label: e.target.value })} + placeholder={t('designer.field.optLabel', locale)} + disabled={disabled} + className="h-7 min-w-0 flex-1 text-xs" + /> +
+
+ update(i, { color: e.target.value })} + disabled={disabled} + className="h-6 w-6 rounded border bg-background cursor-pointer p-0.5" + title={t('designer.field.optColor', locale)} + /> + + + + +
))}
diff --git a/packages/app-shell/src/views/studio-design/BuilderLanding.tsx b/packages/app-shell/src/views/studio-design/BuilderLanding.tsx index caba0f7c4..2239f0369 100644 --- a/packages/app-shell/src/views/studio-design/BuilderLanding.tsx +++ b/packages/app-shell/src/views/studio-design/BuilderLanding.tsx @@ -21,6 +21,7 @@ import { toast } from 'sonner'; import { toFieldNameLoose } from '../metadata-admin/previews/object-fields-io'; import { t, tFormat, useMetadataLocale } from '../metadata-admin/i18n'; import { fetchPackages, createBasePackage, duplicatePackage, PACKAGE_ID_RE, type PkgEntry } from './packages-io'; +import { PackageIdInput, PackageIdSuggestionHint } from './PackageIdInput'; export function BuilderLanding(): React.ReactElement { const navigate = useNavigate(); @@ -168,15 +169,14 @@ export function BuilderLanding(): React.ReactElement { placeholder={t('engine.studio.landing.dupNamePlaceholder', locale)} className="h-7 w-full rounded-md border bg-background px-2 text-[11px] outline-none focus:ring-1 focus:ring-primary" /> - setDupId(e.target.value.toLowerCase().replace(/[^a-z0-9_.-]/g, ''))} - onKeyDown={(e) => { - if (e.key === 'Enter') void doDup(); - if (e.key === 'Escape') setDupFor(null); - }} + onChange={setDupId} + onEnter={() => void doDup()} + onEscape={() => setDupFor(null)} placeholder={t('engine.studio.landing.dupIdPlaceholder', locale)} - className="h-7 w-full rounded-md border bg-background px-2 font-mono text-[11px] outline-none focus:ring-1 focus:ring-primary" + locale={locale} + testId="pkg-dup-id-input" /> {dupErr &&

{dupErr}

}
@@ -222,18 +222,18 @@ export function BuilderLanding(): React.ReactElement { placeholder={t('engine.studio.pkg.namePlaceholder', locale)} className="h-7 w-full rounded-md border bg-background px-2 text-[11px] outline-none focus:ring-1 focus:ring-primary" /> - + { + onChange={(v) => { setIdTouched(true); - setNewId(e.target.value.toLowerCase().replace(/[^a-z0-9_.-]/g, '')); - }} - onKeyDown={(e) => { - if (e.key === 'Enter') void doCreate(); - if (e.key === 'Escape') setCreating(false); + setNewId(v); }} + onEnter={() => void doCreate()} + onEscape={() => setCreating(false)} placeholder={t('engine.studio.pkg.idPlaceholder', locale)} - className="h-7 w-full rounded-md border bg-background px-2 font-mono text-[11px] outline-none focus:ring-1 focus:ring-primary" + locale={locale} + testId="pkg-landing-id-input" />
)}
diff --git a/packages/plugin-view/src/__tests__/ObjectView.test.tsx b/packages/plugin-view/src/__tests__/ObjectView.test.tsx index 72a60051a..5b7f9f4b2 100644 --- a/packages/plugin-view/src/__tests__/ObjectView.test.tsx +++ b/packages/plugin-view/src/__tests__/ObjectView.test.tsx @@ -138,7 +138,10 @@ describe('ObjectView', () => { render(); - expect(screen.getByText('Create')).toBeDefined(); + // The record-create verb now matches the runtime object pages ("New" / + // console.objectView.new); with no I18nProvider mounted it falls back to + // the English default "New" (framework#2615 P3 — verb consistency). + expect(screen.getByText('New')).toBeDefined(); }); it('should hide create button when showCreate is false', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e8c73207..01f0e730f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2405,6 +2405,9 @@ importers: '@object-ui/core': specifier: workspace:* version: link:../core + '@object-ui/i18n': + specifier: workspace:* + version: link:../i18n '@object-ui/plugin-form': specifier: workspace:* version: link:../plugin-form