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
42 changes: 42 additions & 0 deletions .changeset/9950-map-style-reaches-flat-renderer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
'@object-ui/plugin-list': patch
'@object-ui/plugin-view': patch
---

Deliver an authored map `style` on the list-view and object-view paths (objectui#9950).

`ObjectMapConfigSchema` declares `style` ("MapLibre style URL/spec (overrides the
public demo default)"), and both view flatteners dropped it before the renderer ever
saw it. A view authoring `map: { style: 'https://…/style.json' }` parsed green, nothing
refused it, nothing warned, and the map rendered on MapLibre's **public demo tiles**.

Each flattener's key whitelist is now a TOTAL spelling table,
`FLAT_MAP_CONFIG_SPELLING`, mapping every key `ObjectMapConfigSchema` declares to the
name the internal flat form uses for it. Every entry is the identity except `style`,
which is delivered as `mapStyle`.

`mapStyle`, not `style`, because the top-level `style` key is `BaseSchema.style` —
inline CSS, legal on every node. Collapsing the two namespaces onto one key is the
defect objectui#5177 closed and this change deliberately keeps closed: the flatten
still never writes a top-level `style`. `mapStyle` is a declared member of
`ObjectMapSchema` and is the first spelling `getMapConfig` reads
(`schema.mapStyle || schema.map?.style`), so the authored style now reaches the
renderer without inventing an undeclared transport key.

An undeclared key in the `map` block still never reaches the product — that half of
objectui#5177 is unchanged and pinned in both packages' `mapFlatten` suites.

**Why the anti-drift pins did not catch this.** Both sites already pinned their hand
list against the declaration, and both pins were green. They compared the list against
`Object.keys(ObjectMapConfigSchema.shape).filter((key) => key !== 'style')` — the
comparison set had the same key subtracted from it that the whitelist was missing, so
the pin agreed with the omission. Those pins now measure the relation "every declared
key is delivered, under its flat spelling" against the declaration read whole, and each
suite carries a control that feeds the pre-fix whitelist to the same assertion and
shows it rejected. The spelling table is additionally a `Record` over every
`keyof ObjectMapConfig`, so a key added to the declaration fails `tsc` until it is
given a flat spelling.

**Migration.** None. Views that never authored `map.style` are byte-identical in
behaviour; views that did now render with the style they declared instead of the
public demo tiles.
82 changes: 58 additions & 24 deletions packages/plugin-list/src/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,22 @@ import { usePermissions } from '@object-ui/permissions';

/**
* The `case 'map'` branch below builds an `object-map` schema by flattening
* `schema.options.map`'s CONTENTS to the top level. Whitelisted to these keys —
* `ObjectMapConfigSchema`'s shape minus `style` — rather than the whole bag:
* `style` is ALSO `BaseSchema.style` (inline CSS, legal on every node), and
* spreading the raw `map` block collapsed the two namespaces onto one key
* (objectui#5177).
* `schema.options.map`'s CONTENTS to the top level — one entry per key
* `ObjectMapConfigSchema` declares, written under the name the FLAT form uses
* for that key. Whitelisted rather than a whole-bag spread: `style` is ALSO
* `BaseSchema.style` (inline CSS, legal on every node), and spreading the raw
* `map` block collapsed the two namespaces onto one key (objectui#5177). That
* reason is unchanged, and so is its consequence: a key the declaration does
* NOT carry never reaches the product.
*
* `style` IS delivered (objectui#9950) — under its flat spelling `mapStyle`,
* NOT by widening the whitelist to let `style` through unrenamed.
* `getMapConfig` in `ObjectMap.tsx` reads `schema.mapStyle || schema.map?.style`
* and deliberately does NOT read a top-level `style`, because that key is the
* base face's inline CSS (objectui#5017). `mapStyle` is itself a declared
* member of `ObjectMapSchema`, so the flat product stays inside the declaration
* at both ends. Before this, a view authoring `map: { style: '<url>' }` parsed
* green, was dropped here, and the map painted the PUBLIC DEMO TILES.
*
* HAND-LISTED, not derived at runtime — deliberately, and only here (`plugin-
* map`'s own `FLAT_MAP_CONFIG_KEYS` in `ObjectMap.tsx` DOES derive from
Expand All @@ -57,28 +68,50 @@ import { usePermissions } from '@object-ui/permissions';
* gets away with the runtime import only because nothing in
* console-starter's graph reaches `@object-ui/plugin-map` today.
*
* Anti-drift is a TEST, not this comment: `ListView.mapFlatten.test.tsx` pins
* this exact list against `ObjectMapConfigSchema.shape` — imported only from
* that TEST file, which the alias-closure walker explicitly excludes from
* traversal — so a key added to or removed from the declaration still fails
* here, loudly and by name, without reintroducing the runtime edge that
* breaks the walker.
* Anti-drift is TWO mechanisms, neither of them this comment:
* - the type below is TOTAL — a `Record` over EVERY `keyof ObjectMapConfig`,
* not a list of some of them — so a key added to the declaration fails
* `tsc` here until it is given a flat spelling. A key can no longer be
* left out by simply not being written down, which is how `style` was.
* - `ListView.mapFlatten.test.tsx` pins this object's key set against
* `ObjectMapConfigSchema.shape` — imported only from that TEST file, which
* the alias-closure walker explicitly excludes from traversal — and asserts
* the RELATION (every declared key is delivered under its flat spelling).
* The pre-#9950 pin could not see the omission because it compared the hand
* list against `shape` MINUS `style`: the set it measured against was
* narrowed by the same subtraction the defect was made of, so it stayed
* green while an authored style was being discarded.
*/
export const FLAT_MAP_CONFIG_SPELLING = {
latitudeField: 'latitudeField',
longitudeField: 'longitudeField',
locationField: 'locationField',
titleField: 'titleField',
descriptionField: 'descriptionField',
zoom: 'zoom',
center: 'center',
// The one key whose flat spelling differs from its declared name — see the
// objectui#9950 paragraph above for why it is `mapStyle` and not `style`.
style: 'mapStyle',
} as const satisfies Record<keyof ObjectMapConfig, string>;

/**
* Copy the declared map keys an author actually wrote onto the flat product,
* each under its flat spelling.
*
* Values travel AS WRITTEN: this is transport, not a second validation of the
* declared block — that reading belongs to `getMapConfig` in `ObjectMap.tsx`
* and stays there (objectui#5018). Discarding an ill-typed value here would
* reintroduce exactly the silent drop objectui#9950 closed.
*/
export const FLAT_MAP_CONFIG_KEYS = [
'latitudeField',
'longitudeField',
'locationField',
'titleField',
'descriptionField',
'zoom',
'center',
] as const satisfies readonly (keyof Omit<ObjectMapConfig, 'style'>)[];

/** Pick only the declared flat map keys present on an authored `map` block. */
function pickFlatMapConfig(mapConfig: unknown): Record<string, unknown> {
if (!mapConfig || typeof mapConfig !== 'object') return {};
const source = mapConfig as Record<string, unknown>;
return Object.fromEntries(FLAT_MAP_CONFIG_KEYS.filter((key) => key in source).map((key) => [key, source[key]]));
return Object.fromEntries(
Object.entries(FLAT_MAP_CONFIG_SPELLING)
.filter(([declared]) => declared in source)
.map(([declared, flat]) => [flat, source[declared]]),
);
}

/**
Expand Down Expand Up @@ -3108,7 +3141,8 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
};
}
case 'map': {
// Whitelisted flatten (objectui#5177) — see `FLAT_MAP_CONFIG_KEYS`.
// Whitelisted flatten (objectui#5177) — see `FLAT_MAP_CONFIG_SPELLING`,
// which also carries `style` out as `mapStyle` (objectui#9950).
// `schema.options.map` is an untyped bag; a raw spread here forwarded
// every key the author wrote, including `style`, which `ObjectMap`'s
// `FlatMapConfigKeys` declares OUT of this flat form.
Expand Down
Loading
Loading