@object-ui/plugin-map 0.3.1 → 2.0.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,CAsQ9C,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": "2.0.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": "^2.0.7",
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": "2.0.0",
33
+ "@object-ui/core": "2.0.0",
34
+ "@object-ui/react": "2.0.0",
35
+ "@object-ui/types": "2.0.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.13",
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,22 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
183
215
  dataSource,
184
216
  className,
185
217
  onMarkerClick,
218
+ onEdit,
219
+ onDelete,
220
+ ...rest
186
221
  }) => {
187
222
  const [data, setData] = useState<any[]>([]);
188
223
  const [loading, setLoading] = useState(true);
189
224
  const [error, setError] = useState<Error | null>(null);
190
225
  const [objectSchema, setObjectSchema] = useState<any>(null);
191
- const [selectedMarker, setSelectedMarker] = useState<string | null>(null);
226
+ const [selectedMarkerId, setSelectedMarkerId] = useState<string | null>(null);
192
227
 
193
- const dataConfig = getDataConfig(schema);
228
+ const rawDataConfig = getDataConfig(schema);
229
+ // Memoize dataConfig using deep comparison to prevent infinite loops
230
+ const dataConfig = useMemo(() => {
231
+ return rawDataConfig;
232
+ }, [JSON.stringify(rawDataConfig)]);
233
+
194
234
  const mapConfig = getMapConfig(schema);
195
235
  const hasInlineData = dataConfig?.provider === 'value';
196
236
 
@@ -200,6 +240,26 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
200
240
  try {
201
241
  setLoading(true);
202
242
 
243
+ // Prioritize data passed via props (from ListView)
244
+ if ((rest as any).data) { // Check props.data directly first
245
+ const passed = (rest as any).data;
246
+ if (Array.isArray(passed)) {
247
+ setData(passed);
248
+ setLoading(false);
249
+ return;
250
+ }
251
+ }
252
+
253
+ // Check schema.data next
254
+ if ((schema as any).data) {
255
+ const passed = (schema as any).data;
256
+ if (Array.isArray(passed)) {
257
+ setData(passed);
258
+ setLoading(false);
259
+ return;
260
+ }
261
+ }
262
+
203
263
  if (hasInlineData && dataConfig?.provider === 'value') {
204
264
  setData(dataConfig.items as any[]);
205
265
  setLoading(false);
@@ -216,7 +276,18 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
216
276
  $filter: schema.filter,
217
277
  $orderby: convertSortToQueryParams(schema.sort),
218
278
  });
219
- setData(result?.data || []);
279
+
280
+ let items: any[] = [];
281
+ if (Array.isArray(result)) {
282
+ items = result;
283
+ } else if (result && typeof result === 'object') {
284
+ if (Array.isArray((result as any).data)) {
285
+ items = (result as any).data;
286
+ } else if (Array.isArray((result as any).value)) {
287
+ items = (result as any).value;
288
+ }
289
+ }
290
+ setData(items);
220
291
  } else if (dataConfig?.provider === 'api') {
221
292
  console.warn('API provider not yet implemented for ObjectMap');
222
293
  setData([]);
@@ -257,57 +328,73 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
257
328
  }, [schema.objectName, dataSource, hasInlineData, dataConfig]);
258
329
 
259
330
  // Transform data to map markers
260
- const markers = useMemo(() => {
261
- return data
331
+ const { markers, invalidCount } = useMemo(() => {
332
+ let invalid = 0;
333
+ const validMarkers = data
262
334
  .map((record, index) => {
263
335
  const coordinates = extractCoordinates(record, mapConfig);
264
- if (!coordinates) return null;
336
+ if (!coordinates) {
337
+ invalid++;
338
+ return null;
339
+ }
265
340
 
266
341
  const title = mapConfig.titleField ? record[mapConfig.titleField] : 'Marker';
267
342
  const description = mapConfig.descriptionField ? record[mapConfig.descriptionField] : undefined;
268
343
 
344
+ // Ensure lat/lng are within valid ranges
345
+ const [lat, lng] = coordinates;
346
+ if (!isFinite(lat) || !isFinite(lng) || lat < -90 || lat > 90 || lng < -180 || lng > 180) {
347
+ invalid++;
348
+ return null;
349
+ }
350
+
269
351
  return {
270
352
  id: record.id || record._id || `marker-${index}`,
271
353
  title,
272
354
  description,
273
- coordinates,
355
+ coordinates: [lng, lat] as [number, number], // maplibre uses [lng, lat]
274
356
  data: record,
275
357
  };
276
358
  })
277
359
  .filter((marker): marker is NonNullable<typeof marker> => marker !== null);
360
+
361
+ return { markers: validMarkers, invalidCount: invalid };
278
362
  }, [data, mapConfig]);
279
363
 
364
+ const selectedMarker = useMemo(() =>
365
+ markers.find(m => m.id === selectedMarkerId),
366
+ [markers, selectedMarkerId]);
367
+
280
368
  // Calculate map bounds
281
- const bounds = useMemo(() => {
369
+ const initialViewState = useMemo(() => {
282
370
  if (!markers.length) {
283
371
  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,
372
+ longitude: mapConfig.center?.[1] || 0,
373
+ latitude: mapConfig.center?.[0] || 0,
374
+ zoom: mapConfig.zoom || 2
289
375
  };
290
376
  }
291
377
 
292
- const lats = markers.map(m => m.coordinates[0]);
293
- const lngs = markers.map(m => m.coordinates[1]);
378
+ // Simple bounds calculation
379
+ const lngs = markers.map(m => m.coordinates[0]);
380
+ const lats = markers.map(m => m.coordinates[1]);
381
+
382
+ const minLng = Math.min(...lngs);
383
+ const maxLng = Math.max(...lngs);
384
+ const minLat = Math.min(...lats);
385
+ const maxLat = Math.max(...lats);
294
386
 
295
387
  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),
388
+ longitude: (minLng + maxLng) / 2,
389
+ latitude: (minLat + maxLat) / 2,
390
+ zoom: mapConfig.zoom || 3, // Auto-zoom logic could be improved here
304
391
  };
305
- }, [markers, mapConfig.center]);
392
+ }, [markers, mapConfig]);
306
393
 
307
394
  if (loading) {
308
395
  return (
309
396
  <div className={className}>
310
- <div className="flex items-center justify-center h-96 bg-muted rounded-lg">
397
+ <div className="flex items-center justify-center h-96 bg-muted rounded-lg border">
311
398
  <div className="text-muted-foreground">Loading map...</div>
312
399
  </div>
313
400
  </div>
@@ -317,7 +404,7 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
317
404
  if (error) {
318
405
  return (
319
406
  <div className={className}>
320
- <div className="flex items-center justify-center h-96 bg-muted rounded-lg">
407
+ <div className="flex items-center justify-center h-96 bg-muted rounded-lg border">
321
408
  <div className="text-destructive">Error: {error.message}</div>
322
409
  </div>
323
410
  </div>
@@ -326,75 +413,65 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
326
413
 
327
414
  return (
328
415
  <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>
416
+ {invalidCount > 0 && (
417
+ <div className="mb-2 p-2 text-sm text-yellow-800 bg-yellow-50 border border-yellow-200 rounded">
418
+ {`${invalidCount} record${invalidCount !== 1 ? 's' : ''} with missing or invalid coordinates excluded from the map.`}
348
419
  </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
- }}
420
+ )}
421
+ <div className="relative border rounded-lg overflow-hidden bg-muted" style={{ height: '600px', width: '100%' }}>
422
+ <Map
423
+ initialViewState={initialViewState}
424
+ style={{ width: '100%', height: '100%' }}
425
+ mapStyle="https://demotiles.maplibre.org/style.json"
426
+ >
427
+ <NavigationControl position="top-right" />
428
+
429
+ {markers.map(marker => (
430
+ <Marker
431
+ key={marker.id}
432
+ longitude={marker.coordinates[0]}
433
+ latitude={marker.coordinates[1]}
434
+ anchor="bottom"
435
+ onClick={(e) => {
436
+ e.originalEvent.stopPropagation();
437
+ setSelectedMarkerId(marker.id);
438
+ onMarkerClick?.(marker.data);
439
+ }}
371
440
  >
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}
441
+ <div className="text-2xl cursor-pointer hover:scale-110 transition-transform">
442
+ 📍
376
443
  </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>
444
+ </Marker>
445
+ ))}
386
446
 
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>
447
+ {selectedMarker && (
448
+ <Popup
449
+ longitude={selectedMarker.coordinates[0]}
450
+ latitude={selectedMarker.coordinates[1]}
451
+ anchor="top"
452
+ onClose={() => setSelectedMarkerId(null)}
453
+ closeOnClick={false}
454
+ >
455
+ <div className="p-2 min-w-[200px]">
456
+ <h3 className="font-bold text-sm mb-1">{selectedMarker.title}</h3>
457
+ {selectedMarker.description && (
458
+ <p className="text-xs text-muted-foreground">{selectedMarker.description}</p>
459
+ )}
460
+ <div className="mt-2 text-xs flex gap-2">
461
+ {onEdit && (
462
+ <button className="text-blue-500 hover:underline" onClick={() => onEdit(selectedMarker.data)}>Edit</button>
463
+ )}
464
+ {onDelete && (
465
+ <button className="text-red-500 hover:underline" onClick={() => onDelete(selectedMarker.data)}>Delete</button>
466
+ )}
467
+ </div>
468
+ </div>
469
+ </Popup>
470
+ )}
471
+ </Map>
397
472
  </div>
398
473
  </div>
399
474
  );
400
475
  };
476
+
477
+ 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,13 +16,26 @@ 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
- category: 'plugin',
28
+ category: 'view',
29
+ inputs: [
30
+ { name: 'objectName', type: 'string', label: 'Object Name', required: true },
31
+ { name: 'map', type: 'object', label: 'Map Config', description: 'latitudeField, longitudeField, titleField' },
32
+ ],
33
+ });
34
+
35
+ ComponentRegistry.register('map', ObjectMapRenderer, {
36
+ namespace: 'view',
37
+ label: 'Map View',
38
+ category: 'view',
25
39
  inputs: [
26
40
  { name: 'objectName', type: 'string', label: 'Object Name', required: true },
27
41
  { name: 'map', type: 'object', label: 'Map Config', description: 'latitudeField, longitudeField, titleField' },
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
+ });