@object-ui/plugin-map 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,4 +9,5 @@ export interface ObjectMapProps {
9
9
  onDelete?: (record: any) => void;
10
10
  }
11
11
  export declare const ObjectMap: React.FC<ObjectMapProps>;
12
+ export default ObjectMap;
12
13
  //# sourceMappingURL=ObjectMap.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ObjectMap.d.ts","sourceRoot":"","sources":["../../src/ObjectMap.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAuC,MAAM,OAAO,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAY,MAAM,kBAAkB,CAAC;AAE/E,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IACtC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IAC/B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CAClC;AAgJD,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CA2N9C,CAAC"}
1
+ {"version":3,"file":"ObjectMap.d.ts","sourceRoot":"","sources":["../../src/ObjectMap.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAuC,MAAM,OAAO,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAY,MAAM,kBAAkB,CAAC;AAI/E,OAAO,kCAAkC,CAAC;AAY1C,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IACtC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IAC/B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CAClC;AAsKD,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CAyP9C,CAAC;AAEF,eAAe,SAAS,CAAC"}
@@ -1,4 +1,6 @@
1
+ import { default as React } from 'react';
1
2
  import { ObjectMap, ObjectMapProps } from './ObjectMap';
2
3
  export { ObjectMap };
3
4
  export type { ObjectMapProps };
5
+ export declare const ObjectMapRenderer: React.FC<any>;
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,SAAS,EAAE,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,SAAS,EAAE,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,CAAC;AAG/B,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,CAG3C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/plugin-map",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Map visualization plugin for Object UI",
@@ -24,20 +24,24 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
+ "@objectstack/spec": "^0.9.2",
27
28
  "lucide-react": "^0.563.0",
28
- "@object-ui/components": "0.3.1",
29
- "@object-ui/core": "0.3.1",
30
- "@object-ui/react": "0.3.1",
31
- "@object-ui/types": "0.3.1"
29
+ "maplibre-gl": "^5.17.0",
30
+ "react-map-gl": "^8.1.0",
31
+ "zod": "^4.3.6",
32
+ "@object-ui/components": "0.5.0",
33
+ "@object-ui/core": "0.5.0",
34
+ "@object-ui/react": "0.5.0",
35
+ "@object-ui/types": "0.5.0"
32
36
  },
33
37
  "peerDependencies": {
34
38
  "react": "^18.0.0 || ^19.0.0",
35
39
  "react-dom": "^18.0.0 || ^19.0.0"
36
40
  },
37
41
  "devDependencies": {
38
- "@types/react": "^19.2.9",
42
+ "@types/react": "^19.2.10",
39
43
  "@types/react-dom": "^19.2.3",
40
- "@vitejs/plugin-react": "^4.2.1",
44
+ "@vitejs/plugin-react": "^5.1.3",
41
45
  "typescript": "^5.9.3",
42
46
  "vite": "^7.3.1",
43
47
  "vite-plugin-dts": "^4.5.4"
@@ -0,0 +1,110 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor, fireEvent } from '@testing-library/react';
3
+ import { describe, it, expect, vi } from 'vitest';
4
+ import { ObjectMap } from './ObjectMap';
5
+ import { DataSource } from '@object-ui/types';
6
+
7
+ // Mock react-map-gl/maplibre components to check they are receiving the right props/children
8
+ // without relying on real WebGL canvas rendering.
9
+ vi.mock('react-map-gl/maplibre', () => ({
10
+ default: ({ children }: any) => <div aria-label="Map">{children}</div>,
11
+ Map: ({ children }: any) => <div aria-label="Map">{children}</div>,
12
+ NavigationControl: () => <div data-testid="nav-control" />,
13
+ Marker: ({ children, longitude, latitude, onClick }: any) => (
14
+ <div
15
+ data-testid="map-marker"
16
+ data-lat={latitude}
17
+ data-lng={longitude}
18
+ onClick={onClick}
19
+ >
20
+ {children}
21
+ </div>
22
+ ),
23
+ Popup: ({ children }: any) => <div data-testid="map-popup">{children}</div>,
24
+ }));
25
+
26
+ const mockData = [
27
+ { id: '1', name: 'Loc 1', latitude: 40, longitude: -74 },
28
+ { id: '2', name: 'Loc 2', latitude: 41, longitude: -75 },
29
+ ];
30
+
31
+ const mockDataSource: DataSource = {
32
+ find: vi.fn(),
33
+ findOne: vi.fn(),
34
+ create: vi.fn(),
35
+ update: vi.fn(),
36
+ delete: vi.fn(),
37
+ getObjectSchema: vi.fn(),
38
+ };
39
+
40
+ describe('ObjectMap', () => {
41
+ it('renders with static value provider', async () => {
42
+ const schema: any = {
43
+ type: 'map',
44
+ map: {
45
+ latitudeField: 'latitude',
46
+ longitudeField: 'longitude',
47
+ titleField: 'name'
48
+ },
49
+ data: {
50
+ provider: 'value',
51
+ items: mockData
52
+ }
53
+ };
54
+
55
+ render(<ObjectMap schema={schema} />);
56
+
57
+ // Wait for loading to finish
58
+ await waitFor(() => {
59
+ expect(screen.queryByText('Loading map...')).toBeNull();
60
+ });
61
+
62
+ // Check if map is rendered
63
+ expect(screen.getByLabelText('Map')).toBeDefined();
64
+
65
+ // Check if markers are rendered using testid from our mock
66
+ await waitFor(() => {
67
+ const markers = screen.getAllByTestId('map-marker');
68
+ expect(markers).toHaveLength(2);
69
+ // Verify coordinates of first marker
70
+ expect(markers[0]).toHaveAttribute('data-lat', '40');
71
+ expect(markers[0]).toHaveAttribute('data-lng', '-74');
72
+ });
73
+
74
+ // Check content inside marker (the emoji)
75
+ expect(screen.getAllByText('📍')).toHaveLength(2);
76
+ });
77
+
78
+ it('fetches data with object provider', async () => {
79
+ (mockDataSource.find as any).mockResolvedValue({ data: mockData });
80
+
81
+ const schema: any = {
82
+ type: 'map',
83
+ map: {
84
+ latitudeField: 'latitude',
85
+ longitudeField: 'longitude',
86
+ titleField: 'name'
87
+ },
88
+ data: {
89
+ provider: 'object',
90
+ object: 'locations'
91
+ }
92
+ };
93
+
94
+ render(<ObjectMap schema={schema} dataSource={mockDataSource} />);
95
+
96
+ // Wait for loading to finish
97
+ await waitFor(() => {
98
+ expect(screen.queryByText('Loading map...')).toBeNull();
99
+ });
100
+
101
+ // Verify dataSource.find was called
102
+ expect(mockDataSource.find).toHaveBeenCalledWith('locations', expect.anything());
103
+
104
+ // Check markers
105
+ await waitFor(() => {
106
+ const markers = screen.getAllByTestId('map-marker');
107
+ expect(markers).toHaveLength(2);
108
+ });
109
+ });
110
+ });
package/src/ObjectMap.tsx CHANGED
@@ -16,16 +16,26 @@
16
16
  * Features:
17
17
  * - Interactive map with markers
18
18
  * - Location-based data visualization
19
- * - Marker clustering (when many points)
20
19
  * - Popup/tooltip on marker click
21
20
  * - Works with object/api/value data providers
22
- *
23
- * Note: This is a basic implementation. For production use, integrate with a
24
- * proper mapping library like Mapbox, Leaflet, or Google Maps.
25
21
  */
26
22
 
27
23
  import React, { useEffect, useState, useMemo } from 'react';
28
24
  import type { ObjectGridSchema, DataSource, ViewData } from '@object-ui/types';
25
+ import { z } from 'zod';
26
+ import Map, { NavigationControl, Marker, Popup } from 'react-map-gl/maplibre';
27
+ import maplibregl from 'maplibre-gl';
28
+ import 'maplibre-gl/dist/maplibre-gl.css';
29
+
30
+ const MapConfigSchema = z.object({
31
+ latitudeField: z.string().optional(),
32
+ longitudeField: z.string().optional(),
33
+ locationField: z.string().optional(),
34
+ titleField: z.string().optional(),
35
+ descriptionField: z.string().optional(),
36
+ zoom: z.number().optional(),
37
+ center: z.tuple([z.number(), z.number()]).optional(),
38
+ });
29
39
 
30
40
  export interface ObjectMapProps {
31
41
  schema: ObjectGridSchema;
@@ -108,15 +118,37 @@ function convertSortToQueryParams(sort: string | any[] | undefined): Record<stri
108
118
  /**
109
119
  * Helper to get map configuration from schema
110
120
  */
111
- function getMapConfig(schema: ObjectGridSchema): MapConfig {
121
+ function getMapConfig(schema: ObjectGridSchema | any): MapConfig {
122
+ // 1. Check top-level properties (ObjectMapSchema style)
123
+ if (schema.locationField || schema.latitudeField) {
124
+ return {
125
+ locationField: schema.locationField,
126
+ latitudeField: schema.latitudeField,
127
+ longitudeField: schema.longitudeField,
128
+ titleField: schema.titleField || 'name',
129
+ descriptionField: schema.descriptionField,
130
+ zoom: schema.zoom,
131
+ center: schema.center
132
+ };
133
+ }
134
+
135
+ let config: MapConfig | null = null;
112
136
  // Check if schema has map configuration
113
137
  if (schema.filter && typeof schema.filter === 'object' && 'map' in schema.filter) {
114
- return (schema.filter as any).map as MapConfig;
138
+ config = (schema.filter as any).map as MapConfig;
115
139
  }
116
140
 
117
141
  // For backward compatibility, check if schema has map config at root
118
- if ((schema as any).map) {
119
- return (schema as any).map as MapConfig;
142
+ else if ((schema as any).map) {
143
+ config = (schema as any).map as MapConfig;
144
+ }
145
+
146
+ if (config) {
147
+ const result = MapConfigSchema.safeParse(config);
148
+ if (!result.success) {
149
+ console.warn(`[ObjectMap] Invalid map configuration:`, result.error.format());
150
+ }
151
+ return config;
120
152
  }
121
153
 
122
154
  // Default configuration
@@ -183,14 +215,20 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
183
215
  dataSource,
184
216
  className,
185
217
  onMarkerClick,
218
+ ...rest
186
219
  }) => {
187
220
  const [data, setData] = useState<any[]>([]);
188
221
  const [loading, setLoading] = useState(true);
189
222
  const [error, setError] = useState<Error | null>(null);
190
223
  const [objectSchema, setObjectSchema] = useState<any>(null);
191
- const [selectedMarker, setSelectedMarker] = useState<string | null>(null);
224
+ const [selectedMarkerId, setSelectedMarkerId] = useState<string | null>(null);
192
225
 
193
- const dataConfig = getDataConfig(schema);
226
+ const rawDataConfig = getDataConfig(schema);
227
+ // Memoize dataConfig using deep comparison to prevent infinite loops
228
+ const dataConfig = useMemo(() => {
229
+ return rawDataConfig;
230
+ }, [JSON.stringify(rawDataConfig)]);
231
+
194
232
  const mapConfig = getMapConfig(schema);
195
233
  const hasInlineData = dataConfig?.provider === 'value';
196
234
 
@@ -200,6 +238,26 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
200
238
  try {
201
239
  setLoading(true);
202
240
 
241
+ // Prioritize data passed via props (from ListView)
242
+ if ((rest as any).data) { // Check props.data directly first
243
+ const passed = (rest as any).data;
244
+ if (Array.isArray(passed)) {
245
+ setData(passed);
246
+ setLoading(false);
247
+ return;
248
+ }
249
+ }
250
+
251
+ // Check schema.data next
252
+ if ((schema as any).data) {
253
+ const passed = (schema as any).data;
254
+ if (Array.isArray(passed)) {
255
+ setData(passed);
256
+ setLoading(false);
257
+ return;
258
+ }
259
+ }
260
+
203
261
  if (hasInlineData && dataConfig?.provider === 'value') {
204
262
  setData(dataConfig.items as any[]);
205
263
  setLoading(false);
@@ -216,7 +274,18 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
216
274
  $filter: schema.filter,
217
275
  $orderby: convertSortToQueryParams(schema.sort),
218
276
  });
219
- setData(result?.data || []);
277
+
278
+ let items: any[] = [];
279
+ if (Array.isArray(result)) {
280
+ items = result;
281
+ } else if (result && typeof result === 'object') {
282
+ if (Array.isArray((result as any).data)) {
283
+ items = (result as any).data;
284
+ } else if (Array.isArray((result as any).value)) {
285
+ items = (result as any).value;
286
+ }
287
+ }
288
+ setData(items);
220
289
  } else if (dataConfig?.provider === 'api') {
221
290
  console.warn('API provider not yet implemented for ObjectMap');
222
291
  setData([]);
@@ -266,48 +335,58 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
266
335
  const title = mapConfig.titleField ? record[mapConfig.titleField] : 'Marker';
267
336
  const description = mapConfig.descriptionField ? record[mapConfig.descriptionField] : undefined;
268
337
 
338
+ // Ensure lat/lng are within valid ranges
339
+ const [lat, lng] = coordinates;
340
+ if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
341
+ console.warn(`Invalid coordinates for marker ${index}: [${lat}, ${lng}]`);
342
+ return null;
343
+ }
344
+
269
345
  return {
270
346
  id: record.id || record._id || `marker-${index}`,
271
347
  title,
272
348
  description,
273
- coordinates,
349
+ coordinates: [lng, lat] as [number, number], // maplibre uses [lng, lat]
274
350
  data: record,
275
351
  };
276
352
  })
277
353
  .filter((marker): marker is NonNullable<typeof marker> => marker !== null);
278
354
  }, [data, mapConfig]);
279
355
 
356
+ const selectedMarker = useMemo(() =>
357
+ markers.find(m => m.id === selectedMarkerId),
358
+ [markers, selectedMarkerId]);
359
+
280
360
  // Calculate map bounds
281
- const bounds = useMemo(() => {
361
+ const initialViewState = useMemo(() => {
282
362
  if (!markers.length) {
283
363
  return {
284
- center: mapConfig.center || [0, 0],
285
- minLat: (mapConfig.center?.[0] || 0) - 0.1,
286
- maxLat: (mapConfig.center?.[0] || 0) + 0.1,
287
- minLng: (mapConfig.center?.[1] || 0) - 0.1,
288
- maxLng: (mapConfig.center?.[1] || 0) + 0.1,
364
+ longitude: mapConfig.center?.[1] || 0,
365
+ latitude: mapConfig.center?.[0] || 0,
366
+ zoom: mapConfig.zoom || 2
289
367
  };
290
368
  }
291
369
 
292
- const lats = markers.map(m => m.coordinates[0]);
293
- const lngs = markers.map(m => m.coordinates[1]);
370
+ // Simple bounds calculation
371
+ const lngs = markers.map(m => m.coordinates[0]);
372
+ const lats = markers.map(m => m.coordinates[1]);
373
+
374
+ const minLng = Math.min(...lngs);
375
+ const maxLng = Math.max(...lngs);
376
+ const minLat = Math.min(...lats);
377
+ const maxLat = Math.max(...lats);
294
378
 
295
379
  return {
296
- center: [
297
- (Math.min(...lats) + Math.max(...lats)) / 2,
298
- (Math.min(...lngs) + Math.max(...lngs)) / 2,
299
- ] as [number, number],
300
- minLat: Math.min(...lats),
301
- maxLat: Math.max(...lats),
302
- minLng: Math.min(...lngs),
303
- maxLng: Math.max(...lngs),
380
+ longitude: (minLng + maxLng) / 2,
381
+ latitude: (minLat + maxLat) / 2,
382
+ zoom: mapConfig.zoom || 3, // Auto-zoom logic could be improved here
304
383
  };
305
- }, [markers, mapConfig.center]);
384
+ }, [markers, mapConfig]);
306
385
 
307
386
  if (loading) {
308
387
  return (
309
388
  <div className={className}>
310
- <div className="flex items-center justify-center h-96 bg-muted rounded-lg">
389
+ <div className="flex items-center justify-center h-96 bg-muted rounded-lg border">
311
390
  <div className="text-muted-foreground">Loading map...</div>
312
391
  </div>
313
392
  </div>
@@ -317,7 +396,7 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
317
396
  if (error) {
318
397
  return (
319
398
  <div className={className}>
320
- <div className="flex items-center justify-center h-96 bg-muted rounded-lg">
399
+ <div className="flex items-center justify-center h-96 bg-muted rounded-lg border">
321
400
  <div className="text-destructive">Error: {error.message}</div>
322
401
  </div>
323
402
  </div>
@@ -326,75 +405,60 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
326
405
 
327
406
  return (
328
407
  <div className={className}>
329
- <div className="relative border rounded-lg overflow-hidden bg-muted" style={{ height: '600px' }}>
330
- {/* Placeholder map - in production, replace with actual map library */}
331
- <div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-blue-50 to-green-50 dark:from-blue-950 dark:to-green-950">
332
- <div className="text-center">
333
- <div className="text-4xl mb-2">🗺️</div>
334
- <div className="text-sm text-muted-foreground mb-4">
335
- Map Visualization (Placeholder)
336
- </div>
337
- <div className="text-xs text-muted-foreground max-w-md mx-auto">
338
- This is a basic map placeholder. For production, integrate with Mapbox, Leaflet, or Google Maps.
339
- <br />
340
- <br />
341
- <strong>Map Info:</strong>
342
- <br />
343
- Center: [{bounds.center[0].toFixed(4)}, {bounds.center[1].toFixed(4)}]
344
- <br />
345
- Markers: {markers.length}
346
- </div>
347
- </div>
348
- </div>
349
-
350
- {/* Marker List Overlay */}
351
- <div className="absolute top-4 right-4 w-64 bg-background border rounded-lg shadow-lg max-h-96 overflow-y-auto">
352
- <div className="p-3 border-b font-semibold bg-muted">
353
- Locations ({markers.length})
354
- </div>
355
- {markers.length === 0 ? (
356
- <div className="p-4 text-sm text-muted-foreground text-center">
357
- No locations found with valid coordinates
358
- </div>
359
- ) : (
360
- <div>
361
- {markers.map(marker => (
362
- <div
363
- key={marker.id}
364
- className={`p-3 border-b hover:bg-muted/50 cursor-pointer transition-colors ${
365
- selectedMarker === marker.id ? 'bg-muted' : ''
366
- }`}
367
- onClick={() => {
368
- setSelectedMarker(marker.id);
369
- onMarkerClick?.(marker.data);
370
- }}
408
+ <div className="relative border rounded-lg overflow-hidden bg-muted" style={{ height: '600px', width: '100%' }}>
409
+ <Map
410
+ initialViewState={initialViewState}
411
+ style={{ width: '100%', height: '100%' }}
412
+ mapStyle="https://demotiles.maplibre.org/style.json"
413
+ >
414
+ <NavigationControl position="top-right" />
415
+
416
+ {markers.map(marker => (
417
+ <Marker
418
+ key={marker.id}
419
+ longitude={marker.coordinates[0]}
420
+ latitude={marker.coordinates[1]}
421
+ anchor="bottom"
422
+ onClick={(e) => {
423
+ e.originalEvent.stopPropagation();
424
+ setSelectedMarkerId(marker.id);
425
+ onMarkerClick?.(marker.data);
426
+ }}
371
427
  >
372
- <div className="font-medium text-sm">{marker.title}</div>
373
- {marker.description && (
374
- <div className="text-xs text-muted-foreground mt-1 line-clamp-2">
375
- {marker.description}
428
+ <div className="text-2xl cursor-pointer hover:scale-110 transition-transform">
429
+ 📍
376
430
  </div>
377
- )}
378
- <div className="text-xs text-muted-foreground mt-1">
379
- 📍 {marker.coordinates[0].toFixed(4)}, {marker.coordinates[1].toFixed(4)}
380
- </div>
381
- </div>
382
- ))}
383
- </div>
384
- )}
385
- </div>
431
+ </Marker>
432
+ ))}
386
433
 
387
- {/* Legend */}
388
- <div className="absolute bottom-4 left-4 bg-background border rounded-lg shadow-lg p-3">
389
- <div className="text-xs font-semibold mb-2">Legend</div>
390
- <div className="text-xs space-y-1">
391
- <div className="flex items-center gap-2">
392
- <div className="w-3 h-3 rounded-full bg-blue-500" />
393
- <span>Location Marker</span>
394
- </div>
395
- </div>
396
- </div>
434
+ {selectedMarker && (
435
+ <Popup
436
+ longitude={selectedMarker.coordinates[0]}
437
+ latitude={selectedMarker.coordinates[1]}
438
+ anchor="top"
439
+ onClose={() => setSelectedMarkerId(null)}
440
+ closeOnClick={false}
441
+ >
442
+ <div className="p-2 min-w-[200px]">
443
+ <h3 className="font-bold text-sm mb-1">{selectedMarker.title}</h3>
444
+ {selectedMarker.description && (
445
+ <p className="text-xs text-muted-foreground">{selectedMarker.description}</p>
446
+ )}
447
+ <div className="mt-2 text-xs flex gap-2">
448
+ {onEdit && (
449
+ <button className="text-blue-500 hover:underline" onClick={() => onEdit(selectedMarker.data)}>Edit</button>
450
+ )}
451
+ {onDelete && (
452
+ <button className="text-red-500 hover:underline" onClick={() => onDelete(selectedMarker.data)}>Delete</button>
453
+ )}
454
+ </div>
455
+ </div>
456
+ </Popup>
457
+ )}
458
+ </Map>
397
459
  </div>
398
460
  </div>
399
461
  );
400
462
  };
463
+
464
+ export default ObjectMap;
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { describe, it, expect, vi, beforeAll } from 'vitest';
4
+ // import { ComponentRegistry } from '@object-ui/core';
5
+ // Use relative path to force same instance as verified by alias
6
+ import { ObjectMapRenderer } from './index';
7
+ // import { ComponentRegistry } from '@object-ui/core';
8
+
9
+ // Mock dependencies
10
+ vi.mock('@object-ui/react', () => ({
11
+ useSchemaContext: vi.fn(() => ({ dataSource: { type: 'mock-datasource' } })),
12
+ }));
13
+
14
+ vi.mock('./ObjectMap', () => ({
15
+ ObjectMap: ({ dataSource }: any) => (
16
+ <div data-testid="map-mock">
17
+ {dataSource ? `DataSource: ${dataSource.type}` : 'No DataSource'}
18
+ </div>
19
+ )
20
+ }));
21
+
22
+ describe('Plugin Map Registration', () => {
23
+ it('renderer passes dataSource from context', () => {
24
+ render(<ObjectMapRenderer schema={{}} />);
25
+ expect(screen.getByTestId('map-mock')).toHaveTextContent('DataSource: mock-datasource');
26
+ });
27
+ });
package/src/index.tsx CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import React from 'react';
10
10
  import { ComponentRegistry } from '@object-ui/core';
11
+ import { useSchemaContext } from '@object-ui/react';
11
12
  import { ObjectMap } from './ObjectMap';
12
13
  import type { ObjectMapProps } from './ObjectMap';
13
14
 
@@ -15,11 +16,14 @@ export { ObjectMap };
15
16
  export type { ObjectMapProps };
16
17
 
17
18
  // Register component
18
- const ObjectMapRenderer: React.FC<{ schema: any }> = ({ schema }) => {
19
- return <ObjectMap schema={schema} dataSource={null as any} />;
19
+ export const ObjectMapRenderer: React.FC<any> = ({ schema, ...props }) => {
20
+ const { dataSource } = useSchemaContext();
21
+ return <ObjectMap schema={schema} dataSource={dataSource} {...props} />;
20
22
  };
21
23
 
24
+ console.log('Registering object-map...');
22
25
  ComponentRegistry.register('object-map', ObjectMapRenderer, {
26
+ namespace: 'plugin-map',
23
27
  label: 'Object Map',
24
28
  category: 'plugin',
25
29
  inputs: [
package/vite.config.ts CHANGED
@@ -24,6 +24,7 @@ export default defineConfig({
24
24
  resolve: {
25
25
  alias: {
26
26
  '@': resolve(__dirname, './src'),
27
+ '@object-ui/core': resolve(__dirname, '../core/src/index.ts'),
27
28
  },
28
29
  },
29
30
  build: {
@@ -47,4 +48,7 @@ export default defineConfig({
47
48
  },
48
49
  },
49
50
  },
51
+ test: {
52
+ passWithNoTests: true,
53
+ },
50
54
  });
@@ -0,0 +1,13 @@
1
+ /// <reference types="vitest" />
2
+ import { defineConfig } from 'vite';
3
+ import react from '@vitejs/plugin-react';
4
+ import path from 'path';
5
+
6
+ export default defineConfig({
7
+ plugins: [react()],
8
+ test: {
9
+ environment: 'happy-dom',
10
+ globals: true,
11
+ setupFiles: ['./vitest.setup.ts'],
12
+ },
13
+ });