From 6a4d08a5c6271d2b247808747958a0f92d1453e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 22:53:17 +0000 Subject: [PATCH 1/2] fix(app-shell): both dashboard widget-type pickers read one registry derived from the spec The Add-widget menu (11 types) and the widget inspector's Type select (10 types) kept their own hand-written lists while ChartTypeSchema accepts 20, every one of which the dashboard draws. widget-types.ts now takes the offered set from ChartTypeSchema.options and keeps only per-type presentation, checked against ChartType with `satisfies`; the Type select derives its options from the same registry. The nine newly offered types get their names in the studio namespace (en + zh), and the widget dataset hint stops naming the removed inline object query. Claude-Session: https://claude.ai/code/session_01JQwxbafngYyQTJq78seG4v Co-authored-by: Claude --- .../26745-studio-widget-type-registry.md | 13 ++ .../inspectors/DashboardWidgetInspector.tsx | 26 +-- .../inspectorStrings.i18n-10748.test.tsx | 22 +-- .../inspectorStrings.i18n-10804.test.tsx | 20 +-- ...AddWidgetPicker.oneRegistry-26745.test.tsx | 169 ++++++++++++++++++ .../metadata-admin/previews/widget-types.ts | 117 ++++++++---- ui/packages/i18n/src/namespaces/studio/en.ts | 27 ++- ui/packages/i18n/src/namespaces/studio/zh.ts | 21 ++- 8 files changed, 339 insertions(+), 76 deletions(-) create mode 100644 .changeset/26745-studio-widget-type-registry.md create mode 100644 ui/packages/app-shell/src/views/metadata-admin/previews/AddWidgetPicker.oneRegistry-26745.test.tsx diff --git a/.changeset/26745-studio-widget-type-registry.md b/.changeset/26745-studio-widget-type-registry.md new file mode 100644 index 00000000000..2d9b1db0492 --- /dev/null +++ b/.changeset/26745-studio-widget-type-registry.md @@ -0,0 +1,13 @@ +--- +'@object-ui/i18n': patch +--- + +fix(app-shell): the dashboard's Add-widget menu and the widget Type select offer every widget type the spec accepts, from one registry + +Clause-②: no +Metadata-protocol: none + +Studio's two dashboard widget-type pickers kept their own lists and disagreed with each other and with the spec: the Add-widget menu offered 11 types, the inspector's Type select 10 (no `scatter`), and `ChartTypeSchema` accepts 20, all of which a dashboard draws. Both pickers now read one registry derived from `ChartTypeSchema`, so they offer the same 20 types and follow the spec when it changes. + +- The `studio` namespace gains the names of the newly offered types: `engine.inspector.widget.type.*` for `column`, `scatter`, `treemap`, `sankey`, `combo`, `gauge`, `solidGauge`, `kpi`, `bullet` and `radar`, and `engine.widgetPicker.type.*` for the same types except `scatter`, in en and zh. +- `engine.inspector.widget.datasetHint` no longer mentions an "inline object query above", which the widget inspector has not had since ADR-0021; it describes the dataset, dimension and measure controls the inspector shows. diff --git a/ui/packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.tsx b/ui/packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.tsx index c29a8f26a87..274c9d9399a 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.tsx @@ -50,6 +50,7 @@ import { import { useDatasetCatalog, useDatasetSemantics } from '../previews/useDatasetCatalog.js'; import type { ObjectFieldInfo } from '../previews/useObjectFields.js'; import { widgetMissingInputs } from '../../studio-design/metadataError.js'; +import { WIDGET_TYPE_IDS, WIDGET_TYPE_META } from '../previews/widget-types.js'; // ADR-0021: dashboard widgets author the semantic-layer dataset shape only // (dataset + dimensions + values). The pre-ADR-0021 inline single-object query @@ -57,28 +58,19 @@ import { widgetMissingInputs } from '../../studio-design/metadataError.js'; // at @objectstack/spec 9.0.0 and is no longer authored here — its fields are // gone so no Studio surface can emit the dead shape (framework#3251). /** - * The widget families this inspector offers. TYPED to `DashboardWidgetTypeName` - * (objectui#4600, which closed the widget `type` vocabulary) so the inspector - * cannot offer a type validation refuses — every entry here is a spec - * visualization family. A hand-written SUBSET is fine; offering something - * outside the vocabulary is not. + * The widget families this inspector's Type select offers: the ONE registry + * the Add-widget menu reads too (`../previews/widget-types`, objectstack#26745), + * which derives the set from the spec's `ChartTypeSchema` instead of keeping a + * list here. The two pickers used to keep their own lists and disagreed (10 + * here, 11 in the menu, 20 in the spec). ⛔ No hand-kept list in this file. * * Each label is a catalogue key, resolved in the designer locale at render * (objectui#10748, the objectui#10586 `KeyedOption` shape); the stored `value` * never moves. */ -const WIDGET_TYPES: ReadonlyArray<{ value: DashboardWidgetTypeName; labelKey: string }> = [ - { value: 'metric', labelKey: 'engine.inspector.widget.type.metric' }, - { value: 'bar', labelKey: 'engine.inspector.widget.type.bar' }, - { value: 'horizontal-bar', labelKey: 'engine.inspector.widget.type.horizontalBar' }, - { value: 'line', labelKey: 'engine.inspector.widget.type.line' }, - { value: 'area', labelKey: 'engine.inspector.widget.type.area' }, - { value: 'pie', labelKey: 'engine.inspector.widget.type.pie' }, - { value: 'donut', labelKey: 'engine.inspector.widget.type.donut' }, - { value: 'funnel', labelKey: 'engine.inspector.widget.type.funnel' }, - { value: 'table', labelKey: 'engine.inspector.widget.type.table' }, - { value: 'pivot', labelKey: 'engine.inspector.widget.type.pivot' }, -]; +const WIDGET_TYPES: ReadonlyArray<{ value: DashboardWidgetTypeName; labelKey: string }> = WIDGET_TYPE_IDS.map( + (id) => ({ value: id, labelKey: WIDGET_TYPE_META[id].selectLabelKey }), +); const COLORS = [ 'default', diff --git a/ui/packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.i18n-10748.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.i18n-10748.test.tsx index 9de3378689f..19963f46aec 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.i18n-10748.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.i18n-10748.test.tsx @@ -78,6 +78,7 @@ import { ObjectFieldInspector } from './ObjectFieldInspector'; import { FlowEdgeInspector } from './FlowEdgeInspector'; import { FlowNodeInspector } from './FlowNodeInspector'; import { encodeNestedNodeId, NESTED_NODE_KIND } from './flow-nested-selection'; +import { WIDGET_TYPE_META } from '../previews/widget-types'; // The designer's `designer.*` / `perm.*` rows live in `@object-ui/i18n`'s `studio` // namespace, whose `zh` catalogue is fetched on demand (objectstack#25922). // Resident before any case, so the zh rows below read synchronously. @@ -139,19 +140,14 @@ function mountWidget(lang: Lang, draft: Record, selection: { ki )); } -/** Stored widget `type` → the row its option label reads. */ -const WIDGET_TYPE_KEYS: ReadonlyArray<[string, string]> = [ - ['metric', 'engine.inspector.widget.type.metric'], - ['bar', 'engine.inspector.widget.type.bar'], - ['horizontal-bar', 'engine.inspector.widget.type.horizontalBar'], - ['line', 'engine.inspector.widget.type.line'], - ['area', 'engine.inspector.widget.type.area'], - ['pie', 'engine.inspector.widget.type.pie'], - ['donut', 'engine.inspector.widget.type.donut'], - ['funnel', 'engine.inspector.widget.type.funnel'], - ['table', 'engine.inspector.widget.type.table'], - ['pivot', 'engine.inspector.widget.type.pivot'], -]; +/** + * Stored widget `type` → the row its option label reads — read off the one + * widget registry the select derives its options from (objectstack#26745). + */ +const WIDGET_TYPE_KEYS: ReadonlyArray<[string, string]> = Object.values(WIDGET_TYPE_META).map((m) => [ + m.id, + m.selectLabelKey, +]); /** The widget-type select's trigger text, for a widget stored with `type`. */ function widgetTypeTrigger(lang: Lang, type: string): string | null { diff --git a/ui/packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.i18n-10804.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.i18n-10804.test.tsx index d0fc657be8c..426e4e934eb 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.i18n-10804.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.i18n-10804.test.tsx @@ -383,20 +383,12 @@ describe('the Problems panel — expression messages (objectui#10804)', () => { // ─── Site 4: the dashboard add-widget picker (objectui#10805) ──────────────── -/** Stored `type` → the catalogue row its picker entry displays. */ -const WIDGET_TYPE_KEYS: ReadonlyArray<[string, string]> = [ - ['metric', 'engine.widgetPicker.type.metric'], - ['bar', 'engine.widgetPicker.type.bar'], - ['horizontal-bar', 'engine.widgetPicker.type.horizontalBar'], - ['line', 'engine.widgetPicker.type.line'], - ['area', 'engine.widgetPicker.type.area'], - ['pie', 'engine.widgetPicker.type.pie'], - ['donut', 'engine.widgetPicker.type.donut'], - ['scatter', 'engine.widgetPicker.type.scatter'], - ['funnel', 'engine.widgetPicker.type.funnel'], - ['table', 'engine.widgetPicker.type.table'], - ['pivot', 'engine.widgetPicker.type.pivot'], -]; +/** + * Stored `type` → the catalogue row its picker entry displays — read off the + * one widget registry (objectstack#26745), which derives the offered set from + * the spec, so this suite covers every type the picker offers. + */ +const WIDGET_TYPE_KEYS: ReadonlyArray<[string, string]> = Object.values(WIDGET_TYPE_META).map((m) => [m.id, m.labelKey]); const CATEGORIES = ['kpi', 'chart', 'data'] as const; /** Mount the dashboard inspector with no widgets and open its add-widget picker. */ diff --git a/ui/packages/app-shell/src/views/metadata-admin/previews/AddWidgetPicker.oneRegistry-26745.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/previews/AddWidgetPicker.oneRegistry-26745.test.tsx new file mode 100644 index 00000000000..f3d0500a13a --- /dev/null +++ b/ui/packages/app-shell/src/views/metadata-admin/previews/AddWidgetPicker.oneRegistry-26745.test.tsx @@ -0,0 +1,169 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * objectstack#26745 — the dashboard's two widget-type pickers read ONE registry + * of the renderable spec types. + * + * Measured on `main` before this change: the designer's Add-widget menu offered + * 11 types, the widget inspector's Type select 10 (no `scatter`), and the spec's + * `ChartTypeSchema` accepts 20 — every one of which the dashboard draws when + * written through the API. Each picker kept its own hand-written list. + * + * Pinned here, each through the rendered control: + * - the registry IS the spec vocabulary (`WIDGET_TYPE_IDS` = `ChartTypeSchema`); + * - the Add menu offers exactly that set, and so does the Type select; + * - every offered type renders through the real `DashboardRenderer` — no + * "not supported yet" placeholder, no red unknown-component panel; + * - every offered type has its two names in both catalogues (en + zh). + */ + +import '@testing-library/jest-dom/vitest'; +import * as React from 'react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; +import { ChartTypeSchema } from '@objectstack/spec/ui'; +import { enumOptions } from '@object-ui/test-support'; +// Module-scope import of the lazily loaded renderer, so the preview's +// `React.lazy` factory resolves at once instead of racing the test's wait +// window (AGENTS.md, flaky-test discipline). +import '@object-ui/plugin-dashboard'; + +vi.mock('@object-ui/react', async (importOriginal) => { + const mod = await importOriginal(); + return { ...mod, useAdapter: () => ({}) }; +}); +// Network-free dataset catalog for the widget inspector. +vi.mock('./useDatasetCatalog', () => ({ + useDatasetCatalog: () => ({ datasets: [], loading: false, error: null }), + useDatasetSemantics: () => ({ dimensions: [], measures: [], loading: false, error: null }), +})); + +import { t } from '../i18n'; +import { WIDGET_TYPE_IDS, WIDGET_TYPE_META } from './widget-types'; +import { DashboardPreview } from './DashboardPreview'; +import { DashboardDefaultInspector } from '../inspectors/DashboardDefaultInspector'; +import { DashboardWidgetInspector } from '../inspectors/DashboardWidgetInspector'; +// The `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio` namespace, +// whose `zh` catalogue is fetched on demand (objectstack#25922). Resident before +// any case, so the zh rows below read synchronously. +import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio'; +await loadStudioCatalogue('zh'); + +// Radix Select reads pointer capture, which jsdom does not implement. +for (const m of ['hasPointerCapture', 'setPointerCapture', 'releasePointerCapture'] as const) { + if (!(m in Element.prototype)) { + (Element.prototype as unknown as Record)[m] = m === 'hasPointerCapture' ? () => false : () => {}; + } +} +if (!Element.prototype.scrollIntoView) Element.prototype.scrollIntoView = () => {}; + +afterEach(cleanup); + +const specTypes: string[] = enumOptions(ChartTypeSchema); + +async function flush() { + await act(async () => { + await new Promise((r) => setTimeout(r, 0)); + }); +} + +/** The stored types the Add-widget menu lists, read off its rendered entries. */ +async function addMenuTypes(): Promise { + render( + {}} + onSelectionChange={() => {}} + readOnly={false} + />, + ); + fireEvent.click(screen.getByRole('button', { name: t('engine.inspector.add.widget', 'en-US') })); + await flush(); + const picker = screen.getByRole('dialog'); + return Array.from(picker.querySelectorAll('button code')).map((c) => c.textContent ?? ''); +} + +/** The option names the widget inspector's Type select lists. */ +async function typeSelectOptions(): Promise { + render( + {}} + onClearSelection={() => {}} + onSelectionChange={() => {}} + readOnly={false} + />, + ); + fireEvent.keyDown(document.getElementById('widget-type')!, { key: 'ArrowDown' }); + const listbox = await screen.findByRole('listbox'); + return within(listbox) + .getAllByRole('option') + .map((o) => o.textContent ?? ''); +} + +describe('one widget-type registry, derived from the spec (objectstack#26745)', () => { + it('reads a non-empty enum from the spec', () => { + expect(specTypes, 'could not read ChartTypeSchema.options from the spec').not.toEqual([]); + }); + + it('the registry is the spec vocabulary, in the spec order', () => { + expect([...WIDGET_TYPE_IDS]).toEqual(specTypes); + expect(Object.keys(WIDGET_TYPE_META).sort()).toEqual([...specTypes].sort()); + }); + + it('the Add-widget menu offers exactly the spec types', async () => { + expect((await addMenuTypes()).sort()).toEqual([...specTypes].sort()); + }); + + it('the Type select offers exactly the spec types, named from the same registry', async () => { + expect(await typeSelectOptions()).toEqual( + specTypes.map((id) => t(WIDGET_TYPE_META[id].selectLabelKey, 'en-US')), + ); + }); + + it('every offered type renders through the real DashboardRenderer — no placeholder, no red panel', async () => { + const widgets = WIDGET_TYPE_IDS.map((type, i) => ({ + id: `w_${i}`, + type, + title: `Widget ${type}`, + layout: { x: (i % 4) * 3, y: Math.floor(i / 4) * 2, w: 3, h: 2 }, + })); + const { container } = render( + {}} + onPatch={() => {}} + locale="en-US" + />, + ); + await waitFor(() => { + const cells = container.querySelectorAll('[style*="grid-column"]'); + if (cells.length !== widgets.length) throw new Error(`expected ${widgets.length} widget cells, saw ${cells.length}`); + }); + expect(container.textContent).not.toMatch(/chart type is not supported yet/); + expect(container.textContent).not.toMatch(/Unknown component type/); + }); + + it('every offered type has its Add-menu name and its Type-select name in en and zh', () => { + for (const meta of Object.values(WIDGET_TYPE_META)) { + for (const key of [meta.labelKey, meta.selectLabelKey]) { + expect(t(key, 'en-US'), `${key}: missing en row`).not.toBe(key); + // A missing zh row reads the en one back. + expect(t(key, 'zh-CN'), `${key}: missing zh row`).not.toBe(t(key, 'en-US')); + } + // The Add menu's en name is the English the stored default title is written from. + expect(t(meta.labelKey, 'en-US'), meta.id).toBe(meta.label); + } + }); +}); diff --git a/ui/packages/app-shell/src/views/metadata-admin/previews/widget-types.ts b/ui/packages/app-shell/src/views/metadata-admin/previews/widget-types.ts index 554b92a6008..e23b480fb73 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/previews/widget-types.ts +++ b/ui/packages/app-shell/src/views/metadata-admin/previews/widget-types.ts @@ -1,16 +1,40 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. /** - * Catalog of Dashboard widget types — the same set @objectstack/spec - * publishes for `DashboardWidgetSchema.type`. Used by the Dashboard - * designer's "Add widget" picker so authors can choose a chart kind - * up front instead of always starting from `metric` and rebinding. + * The ONE registry of the Dashboard widget types Studio offers. Both pickers + * read it: the designer's "Add widget" menu (`AddWidgetPicker`) and the widget + * inspector's Type select (`DashboardWidgetInspector`). * - * ## Two readers of a type's name (objectui#10804, objectui#10805 folded in) + * ## Derived from the spec, never hand-kept (objectstack#26745) * - * - `labelKey` is what the picker SHOWS: a designer catalogue row + * The offered SET is `@objectstack/spec`'s `ChartTypeSchema` vocabulary, read + * at runtime ({@link WIDGET_TYPE_IDS}) — the closed set + * `DashboardWidgetSchema.type` accepts. The two pickers used to keep their own + * lists, and drifted: the Add menu offered 11 types, the Type select 10 (no + * `scatter`) and the spec accepts 20, so nine types every dashboard surface + * draws had no Studio entry at all. + * + * Every spec type is renderable: the spec lists only the chart families the + * default renderer draws distinctly, and `@object-ui/plugin-dashboard`'s + * `widget-dispatch-spec-parity.test.ts` refuses a spec type that its dispatch + * does not route. `AddWidgetPicker.oneRegistry-26745.test.tsx` re-checks that + * from this side, through the real `DashboardRenderer`. + * + * What stays per type is PRESENTATION — its names, category and icon — in + * {@link WIDGET_TYPE_PRESENTATION}, which is checked against the spec type + * with `satisfies`: a type the spec adds or retires fails the build here until + * it is given (or loses) its row. `list` and `custom` are objectui-only widget + * families with no `ChartTypeSchema` member (`DASHBOARD_WIDGET_TYPE_EXTENSIONS` + * in `@object-ui/types`), so a widget authored with them can never publish + * (framework#3251), and the pickers do not offer them. + * + * ## Readers of a type's name (objectui#10804, objectui#10805 folded in) + * + * - `labelKey` is what the Add menu SHOWS: a designer catalogue row * (`engine.widgetPicker.type.*`), read in the designer locale. Its en row is * the English `label` below, word for word. + * - `selectLabelKey` is what the inspector's Type select shows + * (`engine.inspector.widget.type.*`, objectui#10748). * - `label` is what a new widget's default `New TYPE` title is WRITTEN from * (`DashboardDefaultInspector`, `DashboardPreview`). That title is stored * author data, left literal on purpose (the objectui#10678 / objectui#10651 @@ -23,61 +47,94 @@ import { AreaChart, BarChart, BarChart2, + ChartBarBig, + ChartColumn, + ChartNoAxesCombined, + CircleGauge, Database, Donut, Filter, + Gauge, Hash, + LayoutGrid, LineChart, PieChart, + Radar, ScatterChart, Table2, TrendingDown, + TrendingUp, + Workflow, type LucideIcon, } from 'lucide-react'; +import { ChartTypeSchema, type ChartType } from '@objectstack/spec/ui'; export type WidgetCategory = 'kpi' | 'chart' | 'data'; export interface WidgetTypeMeta { - id: string; + id: ChartType; /** English name the stored default `New TYPE` title is written from — never displayed by the picker. */ label: string; - /** Designer catalogue key of the name the picker displays (`engine.widgetPicker.type.*`). */ + /** Designer catalogue key of the name the Add menu displays (`engine.widgetPicker.type.*`). */ labelKey: string; + /** Designer catalogue key of the name the inspector's Type select displays (`engine.inspector.widget.type.*`). */ + selectLabelKey: string; category: WidgetCategory; icon: LucideIcon; /** Sensible defaults applied when this widget is added. */ defaults?: Record; } -export const WIDGET_TYPE_META: Record = { - metric: { id: 'metric', label: 'Metric (KPI)', labelKey: 'engine.widgetPicker.type.metric', category: 'kpi', icon: Hash }, - bar: { id: 'bar', label: 'Bar chart', labelKey: 'engine.widgetPicker.type.bar', category: 'chart', icon: BarChart }, +/** + * The widget types Studio offers, in the spec's own order: `ChartTypeSchema`'s + * vocabulary, read at runtime so this file restates no list of names. + */ +export const WIDGET_TYPE_IDS: readonly ChartType[] = ChartTypeSchema.options; + +/** + * How each spec widget type is presented. Keyed by the spec's `ChartType` and + * checked with `satisfies`, so it can neither miss a type the spec accepts nor + * name one it does not. + */ +const WIDGET_TYPE_PRESENTATION = { + bar: { label: 'Bar chart', labelKey: 'engine.widgetPicker.type.bar', selectLabelKey: 'engine.inspector.widget.type.bar', category: 'chart', icon: BarChart }, 'horizontal-bar': { - id: 'horizontal-bar', label: 'Horizontal bar', labelKey: 'engine.widgetPicker.type.horizontalBar', + selectLabelKey: 'engine.inspector.widget.type.horizontalBar', category: 'chart', icon: BarChart2, }, - line: { id: 'line', label: 'Line chart', labelKey: 'engine.widgetPicker.type.line', category: 'chart', icon: LineChart }, - area: { id: 'area', label: 'Area chart', labelKey: 'engine.widgetPicker.type.area', category: 'chart', icon: AreaChart }, - pie: { id: 'pie', label: 'Pie chart', labelKey: 'engine.widgetPicker.type.pie', category: 'chart', icon: PieChart }, - donut: { id: 'donut', label: 'Donut chart', labelKey: 'engine.widgetPicker.type.donut', category: 'chart', icon: Donut }, - scatter: { - id: 'scatter', - label: 'Scatter plot', - labelKey: 'engine.widgetPicker.type.scatter', - category: 'chart', - icon: ScatterChart, + column: { label: 'Column chart', labelKey: 'engine.widgetPicker.type.column', selectLabelKey: 'engine.inspector.widget.type.column', category: 'chart', icon: ChartColumn }, + line: { label: 'Line chart', labelKey: 'engine.widgetPicker.type.line', selectLabelKey: 'engine.inspector.widget.type.line', category: 'chart', icon: LineChart }, + area: { label: 'Area chart', labelKey: 'engine.widgetPicker.type.area', selectLabelKey: 'engine.inspector.widget.type.area', category: 'chart', icon: AreaChart }, + pie: { label: 'Pie chart', labelKey: 'engine.widgetPicker.type.pie', selectLabelKey: 'engine.inspector.widget.type.pie', category: 'chart', icon: PieChart }, + donut: { label: 'Donut chart', labelKey: 'engine.widgetPicker.type.donut', selectLabelKey: 'engine.inspector.widget.type.donut', category: 'chart', icon: Donut }, + funnel: { label: 'Funnel', labelKey: 'engine.widgetPicker.type.funnel', selectLabelKey: 'engine.inspector.widget.type.funnel', category: 'chart', icon: TrendingDown }, + scatter: { label: 'Scatter plot', labelKey: 'engine.widgetPicker.type.scatter', selectLabelKey: 'engine.inspector.widget.type.scatter', category: 'chart', icon: ScatterChart }, + treemap: { label: 'Treemap', labelKey: 'engine.widgetPicker.type.treemap', selectLabelKey: 'engine.inspector.widget.type.treemap', category: 'chart', icon: LayoutGrid }, + sankey: { label: 'Sankey diagram', labelKey: 'engine.widgetPicker.type.sankey', selectLabelKey: 'engine.inspector.widget.type.sankey', category: 'chart', icon: Workflow }, + combo: { label: 'Combo chart', labelKey: 'engine.widgetPicker.type.combo', selectLabelKey: 'engine.inspector.widget.type.combo', category: 'chart', icon: ChartNoAxesCombined }, + gauge: { label: 'Gauge', labelKey: 'engine.widgetPicker.type.gauge', selectLabelKey: 'engine.inspector.widget.type.gauge', category: 'kpi', icon: Gauge }, + 'solid-gauge': { + label: 'Solid gauge', + labelKey: 'engine.widgetPicker.type.solidGauge', + selectLabelKey: 'engine.inspector.widget.type.solidGauge', + category: 'kpi', + icon: CircleGauge, }, - funnel: { id: 'funnel', label: 'Funnel', labelKey: 'engine.widgetPicker.type.funnel', category: 'chart', icon: TrendingDown }, - table: { id: 'table', label: 'Data table', labelKey: 'engine.widgetPicker.type.table', category: 'data', icon: Table2 }, - pivot: { id: 'pivot', label: 'Pivot table', labelKey: 'engine.widgetPicker.type.pivot', category: 'data', icon: Database }, - // NOTE: `list` and `custom` are intentionally absent — they are not members - // of @objectstack/spec ChartTypeSchema, so a widget authored with them can - // never publish (framework#3251). Keep this catalog in lockstep with the spec - // enum so the "Add widget" picker only offers publishable types. -}; + metric: { label: 'Metric (KPI)', labelKey: 'engine.widgetPicker.type.metric', selectLabelKey: 'engine.inspector.widget.type.metric', category: 'kpi', icon: Hash }, + kpi: { label: 'KPI', labelKey: 'engine.widgetPicker.type.kpi', selectLabelKey: 'engine.inspector.widget.type.kpi', category: 'kpi', icon: TrendingUp }, + bullet: { label: 'Bullet chart', labelKey: 'engine.widgetPicker.type.bullet', selectLabelKey: 'engine.inspector.widget.type.bullet', category: 'kpi', icon: ChartBarBig }, + radar: { label: 'Radar chart', labelKey: 'engine.widgetPicker.type.radar', selectLabelKey: 'engine.inspector.widget.type.radar', category: 'chart', icon: Radar }, + table: { label: 'Data table', labelKey: 'engine.widgetPicker.type.table', selectLabelKey: 'engine.inspector.widget.type.table', category: 'data', icon: Table2 }, + pivot: { label: 'Pivot table', labelKey: 'engine.widgetPicker.type.pivot', selectLabelKey: 'engine.inspector.widget.type.pivot', category: 'data', icon: Database }, +} satisfies Record>; + +/** Each offered type's presentation, keyed by the stored `type`. */ +export const WIDGET_TYPE_META: Record = Object.fromEntries( + WIDGET_TYPE_IDS.map((id) => [id, { id, ...WIDGET_TYPE_PRESENTATION[id] }]), +); /** Designer catalogue key of each category heading the picker displays. */ export const WIDGET_CATEGORY_LABEL_KEY: Record = { diff --git a/ui/packages/i18n/src/namespaces/studio/en.ts b/ui/packages/i18n/src/namespaces/studio/en.ts index 6225fdbb499..2b72e3c0af6 100644 --- a/ui/packages/i18n/src/namespaces/studio/en.ts +++ b/ui/packages/i18n/src/namespaces/studio/en.ts @@ -500,7 +500,7 @@ const STUDIO_EN: Record = { 'engine.inspector.widget.dataset': 'Dataset', 'engine.inspector.widget.datasetPlaceholder': 'e.g. sales_pipeline', 'engine.inspector.widget.datasetHint': - 'Bind a governed dataset. When set, it takes precedence over the inline object query above and the widget renders via the dataset (consistent, cross-object, RLS-enforced).', + 'Pick the governed dataset this widget draws from, then choose its dimensions and measures below. Dataset numbers stay consistent across objects, and row-level security applies.', 'engine.inspector.widget.dimensions': 'Dimensions', 'engine.inspector.widget.dimensionsPlaceholder': 'e.g. stage, region (comma-separated)', 'engine.inspector.widget.dimensionsHint': @@ -529,6 +529,31 @@ const STUDIO_EN: Record = { 'engine.inspector.widget.type.funnel': 'Funnel', 'engine.inspector.widget.type.table': 'Table', 'engine.inspector.widget.type.pivot': 'Pivot Table', + // objectstack#26745 — the rest of the spec's widget types, now that the Type + // select reads the one widget registry (`previews/widget-types`). + 'engine.inspector.widget.type.column': 'Column Chart', + 'engine.inspector.widget.type.scatter': 'Scatter Plot', + 'engine.inspector.widget.type.treemap': 'Treemap', + 'engine.inspector.widget.type.sankey': 'Sankey Diagram', + 'engine.inspector.widget.type.combo': 'Combo Chart', + 'engine.inspector.widget.type.gauge': 'Gauge', + 'engine.inspector.widget.type.solidGauge': 'Solid Gauge', + 'engine.inspector.widget.type.kpi': 'KPI', + 'engine.inspector.widget.type.bullet': 'Bullet Chart', + 'engine.inspector.widget.type.radar': 'Radar Chart', + // objectstack#26745 — the Add-widget menu's names for the spec widget types it + // did not offer before. The family's other rows still sit in the app-shell + // designer table (objectstack#25922 moves them); each en row is the type's + // English `label` in `previews/widget-types`, word for word. + 'engine.widgetPicker.type.column': 'Column chart', + 'engine.widgetPicker.type.treemap': 'Treemap', + 'engine.widgetPicker.type.sankey': 'Sankey diagram', + 'engine.widgetPicker.type.combo': 'Combo chart', + 'engine.widgetPicker.type.gauge': 'Gauge', + 'engine.widgetPicker.type.solidGauge': 'Solid gauge', + 'engine.widgetPicker.type.kpi': 'KPI', + 'engine.widgetPicker.type.bullet': 'Bullet chart', + 'engine.widgetPicker.type.radar': 'Radar chart', 'engine.inspector.widget.untitledN': 'Widget {n}', 'engine.inspector.widget.unsupportedSelection': 'Unsupported selection kind: {kind}', 'engine.inspector.widget.removed': 'The selected widget was removed from the draft.', diff --git a/ui/packages/i18n/src/namespaces/studio/zh.ts b/ui/packages/i18n/src/namespaces/studio/zh.ts index 3ed1dcf2575..e229577da67 100644 --- a/ui/packages/i18n/src/namespaces/studio/zh.ts +++ b/ui/packages/i18n/src/namespaces/studio/zh.ts @@ -436,7 +436,7 @@ const STUDIO_ZH: Record = { 'engine.inspector.widget.dataset': '数据集', 'engine.inspector.widget.datasetPlaceholder': '例如:sales_pipeline', 'engine.inspector.widget.datasetHint': - '绑定一个受治理的数据集。设置后优先于上方的内联对象查询,组件改为按数据集渲染(口径一致、可跨对象、强制 RLS)。', + '选择本组件取数的受治理数据集,再在下方选择它的维度和度量。数据集口径一致、可跨对象,并强制行级安全(RLS)。', 'engine.inspector.widget.dimensions': '维度', 'engine.inspector.widget.dimensionsPlaceholder': '例如:stage, region(逗号分隔)', 'engine.inspector.widget.dimensionsHint': '用于分组的数据集维度名。留空则渲染为单值 KPI。', @@ -461,6 +461,25 @@ const STUDIO_ZH: Record = { 'engine.inspector.widget.type.funnel': '漏斗图', 'engine.inspector.widget.type.table': '表格', 'engine.inspector.widget.type.pivot': '透视表', + 'engine.inspector.widget.type.column': '柱形图', + 'engine.inspector.widget.type.scatter': '散点图', + 'engine.inspector.widget.type.treemap': '矩形树图', + 'engine.inspector.widget.type.sankey': '桑基图', + 'engine.inspector.widget.type.combo': '组合图', + 'engine.inspector.widget.type.gauge': '仪表图', + 'engine.inspector.widget.type.solidGauge': '实心仪表图', + 'engine.inspector.widget.type.kpi': 'KPI 指标卡', + 'engine.inspector.widget.type.bullet': '子弹图', + 'engine.inspector.widget.type.radar': '雷达图', + 'engine.widgetPicker.type.column': '柱形图', + 'engine.widgetPicker.type.treemap': '矩形树图', + 'engine.widgetPicker.type.sankey': '桑基图', + 'engine.widgetPicker.type.combo': '组合图', + 'engine.widgetPicker.type.gauge': '仪表图', + 'engine.widgetPicker.type.solidGauge': '实心仪表图', + 'engine.widgetPicker.type.kpi': 'KPI 指标卡', + 'engine.widgetPicker.type.bullet': '子弹图', + 'engine.widgetPicker.type.radar': '雷达图', 'engine.inspector.widget.untitledN': '组件 {n}', 'engine.inspector.widget.unsupportedSelection': '不支持的选择类型:{kind}', 'engine.inspector.widget.removed': '所选组件已从草稿中移除。', From 65768a23b8edd4546f605a76e646da6ff29e5d13 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 22:58:58 +0000 Subject: [PATCH 2/2] test(app-shell): the widget render probe registers the chart renderer and carries a lit control Claude-Session: https://claude.ai/code/session_01JQwxbafngYyQTJq78seG4v Co-authored-by: Claude --- ...AddWidgetPicker.oneRegistry-26745.test.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/ui/packages/app-shell/src/views/metadata-admin/previews/AddWidgetPicker.oneRegistry-26745.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/previews/AddWidgetPicker.oneRegistry-26745.test.tsx index f3d0500a13a..1054f597353 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/previews/AddWidgetPicker.oneRegistry-26745.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/previews/AddWidgetPicker.oneRegistry-26745.test.tsx @@ -27,6 +27,8 @@ import { enumOptions } from '@object-ui/test-support'; // `React.lazy` factory resolves at once instead of racing the test's wait // window (AGENTS.md, flaky-test discipline). import '@object-ui/plugin-dashboard'; +// The console registers the chart renderer the series families draw through. +import '@object-ui/plugin-charts'; vi.mock('@object-ui/react', async (importOriginal) => { const mod = await importOriginal(); @@ -128,8 +130,9 @@ describe('one widget-type registry, derived from the spec (objectstack#26745)', ); }); - it('every offered type renders through the real DashboardRenderer — no placeholder, no red panel', async () => { - const widgets = WIDGET_TYPE_IDS.map((type, i) => ({ + /** Render one widget per type through the real renderer; the drawn text once every cell is laid out. */ + async function renderedText(types: readonly string[]): Promise { + const widgets = types.map((type, i) => ({ id: `w_${i}`, type, title: `Widget ${type}`, @@ -151,8 +154,17 @@ describe('one widget-type registry, derived from the spec (objectstack#26745)', const cells = container.querySelectorAll('[style*="grid-column"]'); if (cells.length !== widgets.length) throw new Error(`expected ${widgets.length} widget cells, saw ${cells.length}`); }); - expect(container.textContent).not.toMatch(/chart type is not supported yet/); - expect(container.textContent).not.toMatch(/Unknown component type/); + return container.textContent ?? ''; + } + + it('every offered type renders through the real DashboardRenderer — no placeholder, no red panel', async () => { + const text = await renderedText(WIDGET_TYPE_IDS); + expect(text).not.toMatch(/chart type is not supported yet/); + expect(text).not.toMatch(/Unknown component type/); + }); + + it('control: the same probe sees the placeholder a family with no renderer draws', async () => { + expect(await renderedText(['heatmap'])).toMatch(/chart type is not supported yet/); }); it('every offered type has its Add-menu name and its Type-select name in en and zh', () => {