From 2e22afe2fe863b0ad937a61b2ce624b60e265dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AE=B7=E4=BA=AE=E8=BE=89?= Date: Mon, 17 Aug 2026 15:20:35 +0000 Subject: [PATCH 1/2] =?UTF-8?q?docs(plugin-map):=20README=20=E6=8C=89?= =?UTF-8?q?=E7=8E=B0=E7=A0=81=E9=87=8D=E5=86=99,=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E6=95=99=E5=8C=85=E9=87=8C=E4=B8=8D=E5=AD=98=E5=9C=A8=E7=9A=84?= =?UTF-8?q?=E9=82=A3=E5=A5=97=20API=20(#5002)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `packages/plugin-map/README.md` 教的是一个不存在的组件:作者手写的 `markers` 数组、`layers`、`height`、`useGeolocation`、`center: { lat, lng }` 对象、 `zoom` 的「Default: 10」,以及 `import { mapComponents }` + `Object.entries()` 的手动注册。这些键在 `packages/plugin-map/src` 里没有任何读取点,`mapComponents` 这个导出不存在。照抄任一片段的结果:上下文里有 dataSource 时是一张空地图且 零诊断(不会有「记录坐标无效」提示——根本没有记录进入管线),没有 dataSource 时反而弹一条指向别处的 `Error: DataSource required for object/api providers`。 两种情况下都没有任何一层告诉作者他写的键从未被读。 组件真正读的是查询(`objectName` / `staticData` / `data`,`filter` 与 `sort` 是查询自己的过滤与排序)加上注册时声明的 `map` 块(`latitudeField`、 `longitudeField`、`locationField`、`titleField`、`descriptionField`、`zoom`、 `center` 为 `[latitude, longitude]` 元组、`style`),注册是 import 的副作用。 相机语义按 #4941 / PR #5000:没有默认 zoom,未声明相机即取景到记录集(48px padding、城市级 zoom 上限 12,包围盒沿最短弧度量),空集开在整个世界,声明的 一半胜出、另一半推导,读不出来的 `center` 被诊断而不被适配、也不夺走取景。 ## 形态:对齐重写,但按减法做 新文比旧文短 116 行。完整的作者参考只留一份 —— `content/docs/plugins/plugin-map.mdx`,它被 `check-doc-component-types` 扫、 且刚由 PR #5000 校正过;README 只保留包本地的事实:注册了什么、导出了什么、 两种 provider 各一个能跑的 schema、相机规则,以及读者无法自行推断的两个陷阱: - `map` 块**替换**字段名默认值而不是与之合并 —— `map: { titleField: 'name' }` 单独写就没有命名任何坐标字段,放不下任何 marker; - 不是二元数组的 `center` 会被 `MapConfigSchema` 拒绝并告警,不被适配。 在一份没有门禁的第二副本里重述整个 schema,正是这次漂移的生成器(与 #3881 同形),所以没有把 docs 页的参考再抄一遍。另加一节「本组件不读什么」,直接 点名旧文那批虚构键,让照着旧文(或照着记住了旧文的模型)写的读者能立刻对上。 ## 核实方式 README 里每一行断言都对着 `packages/plugin-map/src` 逐条核过,并用一份临时 探针(未入库)把每个片段与每条断言在真包上跑了一遍:22 例全绿,含两个片段的 渲染、`$filter`/`$orderby` 透传且无 `$top`、`map` 块不合并默认值、`Marker` 兜底标题、fit 的 padding/上限、跨反经线取景、空集世界视图、`center` 的 [lat, lng] 顺序、半声明相机的两种推导、对象形 `center` 的告警且保 fit、 `locationField` 三种坐标形态、越界记录被排除并计数、`markers`/`layers`/ `height`/`useGeolocation` 全部无效果、导出面恰为 `ObjectMap` 与 `ObjectMapRenderer`、`object-map` 与 `map` 两个类型的自注册、聚合在 100 个 marker 以上自动开启、以及 schema 上直接写 `onMarkerClick` 经 `SchemaRenderer` 的属性摊平确实到达组件。 反向验证(方向先判后跑):把旧 README 的片段逐字跑一遍,预判是 0 个 marker 且不提及那些键 —— 6 例全绿,并修正了 issue 正文一处措辞:「空地图且零诊断」 只在上下文有 dataSource 时成立,没有 dataSource 时是上面那条指错方向的报错。 兄弟包 README 巡查按裁定只查不改,同族漂移另立单:#5010(calendar)、 #5011(form)、#5012(gantt)、#5013(grid)、#5014(view)、#5015(dashboard)、 #5016(report);顺带记账 #5017(顶层 `style` 与 `BaseSchema.style` 撞车)、 #5018(`ObjectMapSchema` 未建模 `map` 块)、#5019(docs 页两处残留不符)、 #5020(`clusterRadius` 单位注释,观察类)。 Co-authored-by: Claude --- .changeset/plugin-map-readme-truth-5002.md | 25 ++ packages/plugin-map/README.md | 356 +++++++-------------- 2 files changed, 145 insertions(+), 236 deletions(-) create mode 100644 .changeset/plugin-map-readme-truth-5002.md diff --git a/.changeset/plugin-map-readme-truth-5002.md b/.changeset/plugin-map-readme-truth-5002.md new file mode 100644 index 0000000000..17393a2fdd --- /dev/null +++ b/.changeset/plugin-map-readme-truth-5002.md @@ -0,0 +1,25 @@ +--- +--- + +Docs only: `packages/plugin-map/README.md` is rewritten against the code it documents +(objectui#5002). The old text taught a component that does not exist — an authored +`markers` array, `layers`, `height`, `useGeolocation`, a `center: { lat, lng }` object, +a `zoom` "default: 10", and an `import { mapComponents }` + `Object.entries()` manual +registration — so every snippet in it rendered an empty map with no diagnostic. What the +component actually reads is the ObjectQL query (`objectName` / `staticData` / `data`, +with `filter` and `sort` as the query's own) plus the declared `map` block +(`latitudeField`, `longitudeField`, `locationField`, `titleField`, `descriptionField`, +`zoom`, `center` as a `[latitude, longitude]` tuple, `style`), and registration is a side +effect of the import. + +The rewrite is deliberately shorter than what it replaces: the exhaustive authoring +reference stays in the one gated copy, `content/docs/plugins/plugin-map.mdx`, and the +README keeps only what is local to the package — what it registers, what it exports, one +working schema of each provider shape, the camera rule since objectui#4941/#5000 (no +default zoom: an undeclared camera fits the records), and the traps a reader cannot infer +(a `map` block replaces the field-name defaults instead of merging with them; an +unreadable `center` is diagnosed, not adapted). Restating the whole schema in an ungated +second copy is what produced this drift, the same failure shape as objectui#3881. + +No code, types, or runtime behaviour change, so this declares no release; the corrected +README reaches npm with `@object-ui/plugin-map`'s next publish. diff --git a/packages/plugin-map/README.md b/packages/plugin-map/README.md index c38bdb7366..197d7cfc42 100644 --- a/packages/plugin-map/README.md +++ b/packages/plugin-map/README.md @@ -1,15 +1,16 @@ # @object-ui/plugin-map -Map visualization plugin for Object UI - Display geographic data with interactive maps. +Map view plugin for Object UI. -## Features +Renders the records of an **ObjectQL query** as markers on a MapLibre map: every +marker comes from a record's own coordinate fields, and the first paint frames the +records that were fetched. It is a *view over data* — there is no authored marker +list, and no pin you place by hand. -- **Interactive Maps** - Zoomable, pannable map visualization -- **Markers** - Add markers with custom icons and popups -- **Layers** - Multiple map layers support -- **Geolocation** - User location detection -- **Customizable** - Tailwind CSS styling support -- **Responsive** - Mobile-friendly map controls +Importing the package registers two component types on the `ComponentRegistry`: + +- `object-map` — the object-bound renderer +- `map` — the view-type alias used by `ObjectView` / `ViewSwitcher` ## Installation @@ -19,261 +20,144 @@ pnpm add @object-ui/plugin-map ## Usage -### Automatic Registration (Side-Effect Import) +Registration is a side effect of the import. There is no manual-registration +export to iterate over — the import *is* the registration. -```typescript -// In your app entry point (e.g., App.tsx or main.tsx) +```ts import '@object-ui/plugin-map'; -// Now you can use map types in your schemas -const schema = { - type: 'map', - center: { lat: 37.7749, lng: -122.4194 }, - zoom: 12, - markers: [ - { lat: 37.7749, lng: -122.4194, label: 'San Francisco' } - ] -}; -``` - -### Manual Registration - -```typescript -import { mapComponents } from '@object-ui/plugin-map'; -import { ComponentRegistry } from '@object-ui/core'; - -// Register map components -Object.entries(mapComponents).forEach(([type, component]) => { - ComponentRegistry.register(type, component); -}); -``` - -## Schema API - -### Map - -Display an interactive map: - -```typescript -{ - type: 'map', - center: { lat: number, lng: number }, - zoom?: number, // Default: 10 - markers?: MapMarker[], - layers?: MapLayer[], - height?: number | string, - onMarkerClick?: (marker) => void, - className?: string -} -``` - -### Map Marker - -```typescript -interface MapMarker { - lat: number; - lng: number; - label?: string; - icon?: string; // Icon URL or name - popup?: string | ReactNode; - color?: string; // Marker color -} -``` - -## Examples - -### Basic Map - -```typescript -const schema = { - type: 'map', - center: { lat: 40.7128, lng: -74.0060 }, - zoom: 13, - height: 500, - markers: [ - { - lat: 40.7128, - lng: -74.0060, - label: 'New York City', - popup: 'The Big Apple' - } - ] -}; -``` - -### Multiple Markers - -```typescript -const schema = { - type: 'map', - center: { lat: 37.7749, lng: -122.4194 }, - zoom: 10, - markers: [ - { - lat: 37.7749, - lng: -122.4194, - label: 'San Francisco', - color: 'red', - popup: 'Golden Gate Bridge' - }, - { - lat: 37.8044, - lng: -122.2712, - label: 'Oakland', - color: 'blue', - popup: 'Port of Oakland' - }, - { - lat: 37.3382, - lng: -121.8863, - label: 'San Jose', - color: 'green', - popup: 'Silicon Valley' - } - ] -}; -``` - -### Interactive Map - -```typescript +// Object-bound: the markers are the records the query returns. const schema = { - type: 'map', - center: { lat: 51.5074, lng: -0.1278 }, - zoom: 12, - markers: [/* markers */], - onMarkerClick: (marker) => { - console.log('Marker clicked:', marker); - // Show marker details + type: 'object-map', + objectName: 'stores', + map: { + latitudeField: 'lat', + longitudeField: 'lng', + titleField: 'name', + descriptionField: 'address', }, - onMapClick: (coordinates) => { - console.log('Map clicked at:', coordinates); - // Add new marker - } }; ``` -### Map with Data Binding - -```typescript -const schema = { - type: 'map', - center: { lat: 37.7749, lng: -122.4194 }, - zoom: 10, - markers: '${data.locations.map(loc => ({ lat: loc.latitude, lng: loc.longitude, label: loc.name }))}', - onMarkerClick: (marker) => { - // Handle marker click - } -}; -``` - -## Integration with ObjectQL - -Connect map to ObjectStack data sources: - -```typescript -import { createObjectStackAdapter } from '@object-ui/data-objectstack'; - -const dataSource = createObjectStackAdapter({ - baseUrl: 'https://api.example.com', - token: 'your-auth-token' -}); +A literal record array instead of a query, with the same `map` block: +```ts const schema = { type: 'object-map', - dataSource, - object: 'locations', - latField: 'latitude', - lngField: 'longitude', - labelField: 'name', - popupField: 'description', - center: { lat: 37.7749, lng: -122.4194 }, - zoom: 10 + staticData: [ + { id: 1, name: 'San Francisco HQ', lat: 37.7749, lng: -122.4194 }, + { id: 2, name: 'Oakland Office', lat: 37.8044, lng: -122.2711 }, + ], + map: { latitudeField: 'lat', longitudeField: 'lng', titleField: 'name' }, }; ``` -## Map Features - -### Custom Markers - -```typescript -const schema = { - type: 'map', - markers: [ - { - lat: 40.7128, - lng: -74.0060, - icon: '/custom-marker.png', - popup: { - type: 'card', - title: 'Location Details', - body: 'Custom popup content' - } - } - ] -}; +`filter` and `sort` are the **query's** filter and order — they reach the data +source as `$filter` / `$orderby`, and the spec's per-element `dataSource` binding +is honoured as well. The map issues no row cap of its own. + +## The `map` block + +The declared configuration input. Every key is optional: + +| Key | Description | +| --- | --- | +| `latitudeField` | Record field holding the latitude. Needs `longitudeField` alongside it; both values must be numbers. | +| `longitudeField` | Record field holding the longitude. | +| `locationField` | Single field holding both coordinates — see the formats below. Used when the lat/lng pair yields nothing. | +| `titleField` | Field shown as the marker title. Omitted, markers are titled `Marker`. | +| `descriptionField` | Field shown under the title in the marker popup. | +| `zoom` | Zoom level. Declaring it opts this view out of the auto-fit (see below). | +| `center` | `[latitude, longitude]` — a two-number **tuple**, latitude first. Declaring it opts this view out of the auto-fit. | +| `style` | MapLibre style URL/spec, replacing the default public demo style. | + +**The block replaces the field-name defaults, it is not merged with them.** With +no map configuration at all the component falls back to the field names +`latitude` / `longitude` / `location` / `name` / `description`; the moment a `map` +block is present, only what it declares is read. So `map: { titleField: 'name' }` +on its own names no coordinate field, places nothing, and renders an empty map +under the excluded-records notice — the defaults do not fill the gap. + +## Initial camera + +There is no default zoom and no default centre. With records to show and no +camera declared, the map **fits the records**: their bounding box, measured along +the shortest arc that contains them (so a set straddling the antimeridian is +framed across the line, not around the far side of the planet), with 48px of +padding and a city-scale zoom ceiling of 12 — a single record does not become a +rooftop view. + +Two cases sit outside the fit: + +- **Nothing placeable** (empty result, or no record yielding coordinates): the + whole world, centred on `0, 0`. +- **A declared camera**: `zoom` or `center` in the `map` block wins and the fit is + skipped. Declaring one half keeps the other derived — `zoom` alone is applied at + the centre of the records, `center` alone at a continental zoom. + +A `center` that is not a two-number tuple (the `{ lat, lng }` object form, say) is +rejected by the config schema, warned about in the console, and **not** adapted — +and it does not cost the view its fit. + +## Coordinate formats + +`locationField` reads any of: + +```ts +{ location: { lat: 37.7749, lng: -122.4194 } } // also latitude/longitude, lon +{ location: '37.7749,-122.4194' } // "lat,lng" +{ location: [37.7749, -122.4194] } // [lat, lng] ``` -### Geolocation +A record whose coordinates are missing, unparseable, or out of range (latitude +beyond ±90, longitude beyond ±180) is left off the map and counted in a notice +above it, rather than being silently dropped or rescued. -```typescript -const schema = { - type: 'map', - useGeolocation: true, // Center map on user's location - zoom: 15, - markers: [] -}; -``` +## What this component does not read -### Map Layers +Keys that look plausible on a map schema but have no read site here: `markers` +(markers are records), `layers`, `height` (the container is a fixed responsive +height, 300px through 600px), `useGeolocation` (the map carries a +user-initiated "show my location" button instead), and per-marker `icon` / +`color` / `popup` styling. A `map` configuration stashed under `filter.map` — a +shape predating the `map` input — is no longer read either, and says so in the +console. -```typescript -const schema = { - type: 'map', - center: { lat: 37.7749, lng: -122.4194 }, - layers: [ - { type: 'heatmap', data: [/* heatmap data */] }, - { type: 'polygon', coordinates: [/* polygon coordinates */], color: 'rgba(255, 0, 0, 0.3)' } - ] -}; -``` - -## TypeScript Support +## Using `ObjectMap` directly -```typescript -import type { MapSchema, MapMarker } from '@object-ui/plugin-map'; +`ObjectMap` (the component), `ObjectMapRenderer` (the registered wrapper, for a +host that registers types itself) and the `ObjectMapProps` type are the package's +exports: -const marker: MapMarker = { - lat: 37.7749, - lng: -122.4194, - label: 'San Francisco', - color: 'red' -}; +```tsx +import { ObjectMap } from '@object-ui/plugin-map'; -const map: MapSchema = { - type: 'map', - center: { lat: 37.7749, lng: -122.4194 }, - zoom: 12, - markers: [marker] -}; + console.log(record)} +/>; ``` -## Customization - -Style the map with Tailwind classes: - -```typescript -const schema = { - type: 'map', - className: 'rounded-lg shadow-xl border-2 border-gray-200', - height: '600px', - center: { lat: 37.7749, lng: -122.4194 } -}; -``` +| Prop | Description | +| --- | --- | +| `schema` | The map schema — the keys above. | +| `dataSource` | Resolves the `object` provider. Not needed for `staticData` or an inline `data` array. | +| `className` | Classes for the wrapper around the map. | +| `onMarkerClick` | Called with the clicked record. | +| `onRowClick` | Record click handler; takes priority over the `navigation` overlay. | +| `onEdit` / `onDelete` | Passing either adds that button to the marker popup (and to the mobile record sheet). | +| `enableClustering` | Forces clustering on; without it, clustering starts above 100 visible markers. | +| `clusterRadius` | Clustering granularity (default `50`): the grid cell is `clusterRadius / 2 ** zoom`, so a larger value groups more aggressively. | + +In a schema-driven page these handlers may equally be authored on the node +itself: `SchemaRenderer` spreads a node's non-metadata properties onto the +component. ## Links -- 📚 [Documentation](https://www.objectui.org/docs/plugins/plugin-map) +- 📚 [Documentation](https://www.objectui.org/docs/plugins/plugin-map) — the full + authoring reference for the schema and the `map` block - 📦 [npm package](https://www.npmjs.com/package/@object-ui/plugin-map) - 📝 [Changelog](./CHANGELOG.md) - 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues) From 757a86db32e8ae20cf2abca632ee593a1c6014b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AE=B7=E4=BA=AE=E8=BE=89?= Date: Mon, 17 Aug 2026 15:24:29 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs(plugin-map):=20=E6=94=B6=E5=9B=9E?= =?UTF-8?q?=E4=B8=80=E5=8F=A5=E6=9F=A5=E4=B8=8D=E5=87=BA=E6=B6=88=E8=B4=B9?= =?UTF-8?q?=E8=80=85=E7=9A=84=E6=96=AD=E8=A8=80=20=E2=80=94=E2=80=94=20`ma?= =?UTF-8?q?p`=20=E6=B3=A8=E5=86=8C=E7=9A=84=E8=AF=B4=E6=98=8E=E6=94=B9?= =?UTF-8?q?=E6=8C=89=E5=AE=9E=E6=B5=8B=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一版把两个注册写成「`map` —— `ObjectView` / `ViewSwitcher` 用的 view-type 别名」,那句是比着 `plugin-tree/README.md` 的同位说法写的,而它在本包上查不出 消费者:`ObjectView` 的 `case 'map'` 把地图视图编译成 `type: 'object-map'` 节点(`packages/plugin-view/src/ObjectView.tsx:770-776`)再交 `SchemaRenderer`, `ViewSwitcher` 那侧的 `map` 只是标签与图标(`ViewSwitcher.tsx:65`/`:79`); 全仓除本包测试外没有任何地方构造 `type: 'map'` 的组件节点。 改成能钉住的两条:`map` 是 spec `ViewTypeSchema` 的成员名 (`packages/types/src/zod/views.zod.ts:25`),给直接用该名字写的节点用;并写明 `ObjectView` 里的 map 视图会被编译成 `object-map`,两个拼法落到同一个组件。 这份 README 的全部意义就是每行都能对上代码,所以宁可把话说窄。 Co-authored-by: Claude --- packages/plugin-map/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/plugin-map/README.md b/packages/plugin-map/README.md index 197d7cfc42..e9a91b93bc 100644 --- a/packages/plugin-map/README.md +++ b/packages/plugin-map/README.md @@ -7,10 +7,13 @@ marker comes from a record's own coordinate fields, and the first paint frames t records that were fetched. It is a *view over data* — there is no authored marker list, and no pin you place by hand. -Importing the package registers two component types on the `ComponentRegistry`: +Importing the package registers two component types on the `ComponentRegistry`, +both resolving to the same renderer: - `object-map` — the object-bound renderer -- `map` — the view-type alias used by `ObjectView` / `ViewSwitcher` +- `map` — the bare spec view-type name (`ViewTypeSchema`'s `'map'`), for a node + authored with it directly. Inside an `ObjectView`, a `map` view is compiled to + an `object-map` node, so both spellings end at the same component. ## Installation