@object-ui/plugin-map 0.3.0 → 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.
- package/.turbo/turbo-build.log +36 -0
- package/CHANGELOG.md +12 -0
- package/dist/index.css +1 -0
- package/dist/index.js +3736 -321
- package/dist/index.umd.cjs +847 -2
- package/dist/maplibre-gl-CNsW26De.js +24161 -0
- package/dist/src/ObjectMap.d.ts +1 -0
- package/dist/src/ObjectMap.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +11 -7
- package/src/ObjectMap.test.tsx +110 -0
- package/src/ObjectMap.tsx +160 -96
- package/src/index.test.tsx +27 -0
- package/src/index.tsx +6 -2
- package/vite.config.ts +4 -0
- package/vitest.config.ts +13 -0
- package/vitest.setup.ts +78 -0
package/dist/src/ObjectMap.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ObjectMap.d.ts","sourceRoot":"","sources":["../../src/ObjectMap.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH
|
|
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"}
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
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
|
+
"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
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"@object-ui/
|
|
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.
|
|
42
|
+
"@types/react": "^19.2.10",
|
|
39
43
|
"@types/react-dom": "^19.2.3",
|
|
40
|
-
"@vitejs/plugin-react": "^
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
224
|
+
const [selectedMarkerId, setSelectedMarkerId] = useState<string | null>(null);
|
|
192
225
|
|
|
193
|
-
const
|
|
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
|
-
|
|
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
|
|
361
|
+
const initialViewState = useMemo(() => {
|
|
282
362
|
if (!markers.length) {
|
|
283
363
|
return {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
293
|
-
const lngs = markers.map(m => m.coordinates[
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
373
|
-
|
|
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
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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<
|
|
19
|
-
|
|
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
|
});
|
package/vitest.config.ts
ADDED
|
@@ -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
|
+
});
|