Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/8885-object-chart-block-props.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@object-ui/plugin-charts': minor
---

**BREAKING** — `ObjectChartBlock`, the registry shell exported beside `ObjectChart`, declares its props instead of taking `(props: any)` (objectui#8885).

**Clause-②: yes** — a published component signature narrows. `@object-ui/plugin-charts` 17.6.0 publishes `ObjectChartBlock: (props: any) => React.JSX.Element`. It is now `(props: Omit<ObjectChartProps, 'schema'> & { schema: BaseSchema }) => React.JSX.Element`. No export is added, removed or renamed, and no runtime behaviour changes.

**FROM** any props bag **TO** `ObjectChart`'s own props, with `schema` as the raw node:

```tsx
// before: all of these compiled
<ObjectChartBlock schema={node} dataSorce={adapter} /> // misspelled prop, silently dropped
<ObjectChartBlock schema="object-chart" /> // a string, not a node
<ObjectChartBlock schema={{ objectName: 'deal' }} /> // a node with no `type`
// after: pass a node (`BaseSchema`: at least `type`) and only ObjectChart's props
<ObjectChartBlock schema={{ type: 'object-chart', objectName: 'deal', chartType: 'bar' }} dataSource={adapter} />
```

`schema` is `BaseSchema`, the node `SchemaRenderer` hands every registered renderer, and not `ObjectChartSchema`. The shell receives the node BEFORE `ElementDataSourceGate` maps its `dataSource` binding, so a node that carries only `{ type, dataSource: { object, view } }` is legal input. So is a node typed `chart`, the alias this same shell is registered under. The other props (`dataSource`, `onSegmentClick`) are `ObjectChartProps`' own, so a misspelled prop name is now an excess-property error.

Migration: a direct caller that passed extra props through the shell must pass only `schema`, `dataSource` and `onSegmentClick`, and a node without `type` must add one. Nodes rendered through the registry (`SchemaRenderer`) are unaffected: the registry resolves renderers as `any`, and the type annotations and the single hand-off cast erase at build.
50 changes: 38 additions & 12 deletions packages/plugin-charts/src/ObjectChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ComponentRegistry, chartMeasureKey, isStructuredGroupBy, objectAggregat
import { Sheet, SheetContent, SheetHeader, SheetTitle, Dialog, DialogContent, DialogHeader, DialogTitle, RefreshIndicator, Button, ChartSkeleton, DataEmptyState } from '@object-ui/components';
import { AlertCircle, ArrowUpRight, Inbox } from 'lucide-react';
import { builtinAggregateLabels, useSafeFieldLabel, useSafeTranslate, useObjectTranslation, pickLocalized } from '@object-ui/i18n';
import type { DrillDownConfig, ObjectChartSchema } from '@object-ui/types';
import type { BaseSchema, DrillDownConfig, ObjectChartSchema } from '@object-ui/types';

/**
* Humanize a snake_case or kebab-case string into Title Case.
Expand Down Expand Up @@ -1546,18 +1546,44 @@ const OBJECT_CHART_DATA_SOURCE: ElementDataSourceMapping = {
* spec documents rendered an empty frame with no error and no request. Lives
* here, beside the registration, rather than in `ChartContainerImpl` — the
* binding is a registry-boundary concern, not a rendering one.
*
* ## Its props type — the node BEFORE the gate (objectui#8885)
*
* This shell used to be published as `(props: any)`. `schema` is now
* `BaseSchema`, the node `SchemaRenderer` hands every registered renderer, and
* ⛔ NOT `ObjectChartSchema`. The node arrives here before the gate has mapped
* its `dataSource` binding, and nothing has validated it as a chart. Typing it
* post-gate would claim a validation that has not happened, and that type's
* `type: 'object-chart'` literal is also false for the `chart` alias that
* `index.tsx` registers this same shell under.
*
* Every other prop is `ObjectChart`'s own, taken from `ObjectChartProps`, so
* the type is CLOSED: a misspelled prop name is an excess-property error, not
* an `any` passed straight through. Both halves are existing exported names;
* no new type is minted. Pinned by
* `__tests__/ObjectChartBlock.props-8885.test.tsx`.
*/
export const ObjectChartBlock = elementDataSourceBlock((props: any) => (
<ElementDataSourceGate
schema={props.schema}
mapping={OBJECT_CHART_DATA_SOURCE}
dataSource={props.dataSource}
testId="object-chart"
errorTitle="This chart’s data source could not be resolved"
>
{(bound) => <ObjectChart {...props} schema={bound} />}
</ElementDataSourceGate>
));
export const ObjectChartBlock = elementDataSourceBlock(
(props: Omit<ObjectChartProps, 'schema'> & { schema: BaseSchema }) => (
<ElementDataSourceGate
schema={props.schema}
mapping={OBJECT_CHART_DATA_SOURCE}
dataSource={props.dataSource}
testId="object-chart"
errorTitle="This chart’s data source could not be resolved"
>
{(bound) => (
// The ONE loose member of this signature, kept on purpose. This is
// where the pre-gate node becomes the component's post-gate schema,
// and `ObjectChart` is where the post-gate claim belongs. The gate
// maps the binding and does not validate, so the claim has to be
// made here, at one line, instead of on the published signature
// above.
<ObjectChart {...props} schema={bound as ObjectChartSchema} />
)}
</ElementDataSourceGate>
),
);

// Register it
ComponentRegistry.register('object-chart', ObjectChartBlock, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* objectui#8885 Part 2: `ObjectChartBlock`, the registry shell around
* `ObjectChart`, gets a declared props type instead of `(props: any)`.
*
* Ruling 5810232641 (maintainer 「同意」), following the objectui#7912 /
* objectui#7483 direction: a published component signature is never bare
* `any`. The shell receives the node BEFORE `ElementDataSourceGate`, so its
* `schema` is the registry renderer's raw node input, `BaseSchema`, and ⛔ not
* the post-gate `ObjectChartSchema`. Every other prop is `ObjectChart`'s own,
* so the type is closed.
*
* ## Why this pin is compile-time
*
* Nothing renders differently. What moved is the ACCEPT SET of a published
* signature, and `tsconfig.test.json` compiles this file, so every statement
* below is enforcement: a `@ts-expect-error` whose refusal stops happening
* fails the build with TS2578. The shell is taken as a TYPE (`import type` plus
* `declare const`), so no module side effect runs here and nothing below is
* ever rendered. The probes live in a function that is never called.
*
* ## The ceiling, stated rather than assumed
*
* A closed PROPS type refuses a misspelled prop NAME. It does not refuse a
* misspelled key INSIDE the node: `BaseSchema` carries `[key: string]: any`,
* the protocol's passthrough. The counter-probe at the bottom keeps that
* visible so nobody reads this pin as more than it is.
*/

import { describe, it, expect } from 'vitest';
import type * as ObjectChartModule from '../ObjectChart';
import type { ObjectChartProps } from '../ObjectChart';
import type { BaseSchema, ObjectChartSchema } from '@object-ui/types';

declare const ObjectChartBlock: typeof ObjectChartModule.ObjectChartBlock;
declare const adapter: unknown;

type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type IsAny<T> = 0 extends 1 & T ? true : false;

/** What the published shell declares it accepts. */
type ShellProps = Parameters<typeof ObjectChartModule.ObjectChartBlock>[0];

// ── The any-guard: the regression this card exists to stop ─────────────────
export type assertionShellParamIsNotAny = Expect<Equal<IsAny<ShellProps>, false>>;
export type assertionShellSchemaIsNotAny = Expect<Equal<IsAny<ShellProps['schema']>, false>>;

// ── The ruling's letter: pre-gate `BaseSchema`, never the post-gate type ────
export type assertionSchemaIsTheRawNode = Expect<Equal<ShellProps['schema'], BaseSchema>>;
export type assertionSchemaIsNotPostGate = Expect<Equal<Equal<ShellProps['schema'], ObjectChartSchema>, false>>;
/** The rest is `ObjectChart`'s own props, nothing more and nothing less. */
export type assertionRestIsObjectChartProps = Expect<
Equal<Omit<ShellProps, 'schema'>, Omit<ObjectChartProps, 'schema'>>
>;

// ── The helpers can FAIL, so a vacuous `Equal` / `IsAny` cannot pass this file ─
export type assertionEqualCanFail = Expect<Equal<Equal<BaseSchema, ObjectChartSchema>, false>>;
export type assertionAnyProbeCanFire = Expect<Equal<IsAny<any>, true>>;

/** Never called: compiled by `tsc -p tsconfig.test.json`, never rendered. */
const probes = () => (
<>
{/* P1: a misspelled prop NAME (`dataSource` spelled `dataSorce`). */}
{/* @ts-expect-error — the props type is closed, so the misspelling is an excess property, not an `any` passed through. */}
<ObjectChartBlock schema={{ type: 'object-chart' }} dataSorce={adapter} />
{/* P2: `schema` itself misspelled, so the required prop is missing. */}
{/* @ts-expect-error — `schema` is required. */}
<ObjectChartBlock schem={{ type: 'object-chart' }} />
{/* P3: a string where the node belongs. */}
{/* @ts-expect-error — `schema` is a node (`BaseSchema`); a bare string is not one. */}
<ObjectChartBlock schema="object-chart" />
{/* P4: a node without `type`. */}
{/* @ts-expect-error — `type` is required on every node; it is the registry key. */}
<ObjectChartBlock schema={{ objectName: 'opportunity' }} />

{/* C1 control: the `chart` alias registration's node. Refused by the
post-gate `ObjectChartSchema` (whose `type` is the literal
'object-chart'), and it must compile here: `index.tsx` registers this
same shell under `chart`. */}
<ObjectChartBlock schema={{ type: 'chart', objectName: 'opportunity', chartType: 'bar' }} />
{/* C2 control: a pre-gate node that carries only the spec's binding. The
gate maps `dataSource.object` onto `objectName`, so the node is legal
input before it has any `objectName` or `chartType` of its own. */}
<ObjectChartBlock schema={{ type: 'object-chart', dataSource: { object: 'opportunity', view: 'all' } }} />
{/* C3 control: `ObjectChart`'s own props still pass straight through the shell. */}
<ObjectChartBlock
schema={{ type: 'object-chart', objectName: 'opportunity', chartType: 'bar' }}
dataSource={adapter}
onSegmentClick={() => undefined}
/>

{/* CEILING, not a refusal: a misspelled key INSIDE the node still
compiles, through `BaseSchema`'s passthrough index signature. */}
<ObjectChartBlock schema={{ type: 'object-chart', objectNme: 'opportunity' }} />
</>
);

describe('ObjectChartBlock props: the pre-gate node, closed (objectui#8885)', () => {
it('is a compile-time pin; the probes type-check under tsconfig.test.json and are never rendered', () => {
expect(typeof probes).toBe('function');
});
});
Loading