@object-ui/plugin-map 3.3.0 → 3.3.1

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/src/ObjectMap.tsx DELETED
@@ -1,633 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * ObjectMap Component
11
- *
12
- * A specialized map visualization component that works with ObjectQL data sources.
13
- * Displays records as markers/pins on a map based on location data.
14
- * Implements the map view type from @objectstack/spec view.zod ListView schema.
15
- *
16
- * Features:
17
- * - Interactive map with markers
18
- * - Location-based data visualization
19
- * - Popup/tooltip on marker click
20
- * - Works with object/api/value data providers
21
- */
22
-
23
- import React, { useEffect, useState, useMemo } from 'react';
24
- import type { ObjectGridSchema, DataSource, ViewData } from '@object-ui/types';
25
- import { useNavigationOverlay } from '@object-ui/react';
26
- import { NavigationOverlay, cn } from '@object-ui/components';
27
- import { extractRecords, buildExpandFields } from '@object-ui/core';
28
- import { z } from 'zod';
29
- import MapGL, { NavigationControl, Marker, Popup } from 'react-map-gl/maplibre';
30
- import maplibregl from 'maplibre-gl';
31
- import 'maplibre-gl/dist/maplibre-gl.css';
32
-
33
- const MapConfigSchema = z.object({
34
- latitudeField: z.string().optional(),
35
- longitudeField: z.string().optional(),
36
- locationField: z.string().optional(),
37
- titleField: z.string().optional(),
38
- descriptionField: z.string().optional(),
39
- zoom: z.number().optional(),
40
- center: z.tuple([z.number(), z.number()]).optional(),
41
- });
42
-
43
- export interface ObjectMapProps {
44
- schema: ObjectGridSchema;
45
- dataSource?: DataSource;
46
- className?: string;
47
- onMarkerClick?: (record: any) => void;
48
- onRowClick?: (record: any) => void;
49
- onEdit?: (record: any) => void;
50
- onDelete?: (record: any) => void;
51
- /** Enable marker clustering for dense data */
52
- enableClustering?: boolean;
53
- /** Cluster radius in pixels (default: 50) */
54
- clusterRadius?: number;
55
- }
56
-
57
- interface MapConfig {
58
- /** Field containing latitude value */
59
- latitudeField?: string;
60
- /** Field containing longitude value */
61
- longitudeField?: string;
62
- /** Field containing combined location (e.g., "lat,lng" or location object) */
63
- locationField?: string;
64
- /** Field to use for marker title/label */
65
- titleField?: string;
66
- /** Field to use for marker description */
67
- descriptionField?: string;
68
- /** Default zoom level (1-20) */
69
- zoom?: number;
70
- /** Center coordinates [lat, lng] */
71
- center?: [number, number];
72
- }
73
-
74
- /**
75
- * Helper to get data configuration from schema
76
- */
77
- function getDataConfig(schema: ObjectGridSchema): ViewData | null {
78
- if (schema.data) {
79
- return schema.data;
80
- }
81
-
82
- if (schema.staticData) {
83
- return {
84
- provider: 'value',
85
- items: schema.staticData,
86
- };
87
- }
88
-
89
- if (schema.objectName) {
90
- return {
91
- provider: 'object',
92
- object: schema.objectName,
93
- };
94
- }
95
-
96
- return null;
97
- }
98
-
99
- /**
100
- * Helper to convert sort config to QueryParams format
101
- */
102
- function convertSortToQueryParams(sort: string | any[] | undefined): Record<string, 'asc' | 'desc'> | undefined {
103
- if (!sort) return undefined;
104
-
105
- // If it's a string like "name desc"
106
- if (typeof sort === 'string') {
107
- const parts = sort.split(' ');
108
- const field = parts[0];
109
- const order = (parts[1]?.toLowerCase() === 'desc' ? 'desc' : 'asc') as 'asc' | 'desc';
110
- return { [field]: order };
111
- }
112
-
113
- // If it's an array of SortConfig objects
114
- if (Array.isArray(sort)) {
115
- return sort.reduce((acc, item) => {
116
- if (item.field && item.order) {
117
- acc[item.field] = item.order;
118
- }
119
- return acc;
120
- }, {} as Record<string, 'asc' | 'desc'>);
121
- }
122
-
123
- return undefined;
124
- }
125
-
126
- /**
127
- * Helper to get map configuration from schema
128
- */
129
- function getMapConfig(schema: ObjectGridSchema | any): MapConfig {
130
- // 1. Check top-level properties (ObjectMapSchema style)
131
- if (schema.locationField || schema.latitudeField) {
132
- return {
133
- locationField: schema.locationField,
134
- latitudeField: schema.latitudeField,
135
- longitudeField: schema.longitudeField,
136
- titleField: schema.titleField || 'name',
137
- descriptionField: schema.descriptionField,
138
- zoom: schema.zoom,
139
- center: schema.center
140
- };
141
- }
142
-
143
- let config: MapConfig | null = null;
144
- // Check if schema has map configuration
145
- if (schema.filter && typeof schema.filter === 'object' && 'map' in schema.filter) {
146
- config = (schema.filter as any).map as MapConfig;
147
- }
148
-
149
- // For backward compatibility, check if schema has map config at root
150
- else if ((schema as any).map) {
151
- config = (schema as any).map as MapConfig;
152
- }
153
-
154
- if (config) {
155
- const result = MapConfigSchema.safeParse(config);
156
- if (!result.success) {
157
- console.warn(`[ObjectMap] Invalid map configuration:`, result.error.format());
158
- }
159
- return config;
160
- }
161
-
162
- // Default configuration
163
- return {
164
- latitudeField: 'latitude',
165
- longitudeField: 'longitude',
166
- locationField: 'location',
167
- titleField: 'name',
168
- descriptionField: 'description',
169
- zoom: 10,
170
- center: [0, 0],
171
- };
172
- }
173
-
174
- /**
175
- * Extract coordinates from a record based on configuration
176
- */
177
- function extractCoordinates(record: any, config: MapConfig): [number, number] | null {
178
- // Try latitude/longitude fields
179
- if (config.latitudeField && config.longitudeField) {
180
- const lat = record[config.latitudeField];
181
- const lng = record[config.longitudeField];
182
- if (typeof lat === 'number' && typeof lng === 'number') {
183
- return [lat, lng];
184
- }
185
- }
186
-
187
- // Try location field
188
- if (config.locationField) {
189
- const location = record[config.locationField];
190
-
191
- // Handle object format: { lat: number, lng: number }
192
- if (typeof location === 'object' && location !== null) {
193
- const lat = location.lat || location.latitude;
194
- const lng = location.lng || location.lon || location.longitude;
195
- if (typeof lat === 'number' && typeof lng === 'number') {
196
- return [lat, lng];
197
- }
198
- }
199
-
200
- // Handle string format: "lat,lng"
201
- if (typeof location === 'string') {
202
- const parts = location.split(',').map(s => parseFloat(s.trim()));
203
- if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
204
- return [parts[0], parts[1]];
205
- }
206
- }
207
-
208
- // Handle array format: [lat, lng]
209
- if (Array.isArray(location) && location.length === 2) {
210
- const lat = parseFloat(location[0]);
211
- const lng = parseFloat(location[1]);
212
- if (!isNaN(lat) && !isNaN(lng)) {
213
- return [lat, lng];
214
- }
215
- }
216
- }
217
-
218
- return null;
219
- }
220
-
221
- interface MarkerData {
222
- id: string;
223
- title: string;
224
- description?: string;
225
- coordinates: [number, number];
226
- data: any;
227
- }
228
-
229
- interface ClusterData {
230
- id: string;
231
- coordinates: [number, number];
232
- markers: MarkerData[];
233
- isCluster: boolean;
234
- }
235
-
236
- /**
237
- * Simple grid-based marker clustering.
238
- * Groups markers that are close to each other at a given zoom level.
239
- */
240
- function clusterMarkers(markers: MarkerData[], zoom: number, radius: number = 50): ClusterData[] {
241
- if (markers.length <= 1 || zoom >= 15) {
242
- return markers.map(m => ({
243
- id: m.id,
244
- coordinates: m.coordinates,
245
- markers: [m],
246
- isCluster: false,
247
- }));
248
- }
249
-
250
- // Grid cell size based on zoom (larger cells at lower zoom)
251
- const cellSize = radius / Math.pow(2, zoom);
252
- const grid = new Map<string, MarkerData[]>();
253
-
254
- markers.forEach(marker => {
255
- const cellX = Math.floor(marker.coordinates[0] / cellSize);
256
- const cellY = Math.floor(marker.coordinates[1] / cellSize);
257
- const key = `${cellX}:${cellY}`;
258
- if (!grid.has(key)) grid.set(key, []);
259
- grid.get(key)!.push(marker);
260
- });
261
-
262
- const clusters: ClusterData[] = [];
263
- grid.forEach((group, key) => {
264
- if (group.length === 1) {
265
- clusters.push({
266
- id: group[0].id,
267
- coordinates: group[0].coordinates,
268
- markers: group,
269
- isCluster: false,
270
- });
271
- } else {
272
- // Compute centroid
273
- const avgLng = group.reduce((sum, m) => sum + m.coordinates[0], 0) / group.length;
274
- const avgLat = group.reduce((sum, m) => sum + m.coordinates[1], 0) / group.length;
275
- clusters.push({
276
- id: `cluster-${key}`,
277
- coordinates: [avgLng, avgLat],
278
- markers: group,
279
- isCluster: true,
280
- });
281
- }
282
- });
283
-
284
- return clusters;
285
- }
286
-
287
- export const ObjectMap: React.FC<ObjectMapProps> = ({
288
- schema,
289
- dataSource,
290
- className,
291
- onMarkerClick,
292
- onRowClick,
293
- onEdit,
294
- onDelete,
295
- enableClustering,
296
- clusterRadius = 50,
297
- ...rest
298
- }) => {
299
- const [data, setData] = useState<any[]>([]);
300
- const [loading, setLoading] = useState(true);
301
- const [error, setError] = useState<Error | null>(null);
302
- const [objectSchema, setObjectSchema] = useState<any>(null);
303
- const [selectedMarkerId, setSelectedMarkerId] = useState<string | null>(null);
304
- const [searchQuery, setSearchQuery] = useState('');
305
-
306
- const rawDataConfig = getDataConfig(schema);
307
- // Memoize dataConfig using deep comparison to prevent infinite loops
308
- const dataConfig = useMemo(() => {
309
- return rawDataConfig;
310
- }, [JSON.stringify(rawDataConfig)]);
311
-
312
- const mapConfig = getMapConfig(schema);
313
- const hasInlineData = dataConfig?.provider === 'value';
314
-
315
- // Fetch data based on provider
316
- useEffect(() => {
317
- const fetchData = async () => {
318
- try {
319
- setLoading(true);
320
-
321
- // Prioritize data passed via props (from ListView)
322
- if ((rest as any).data) { // Check props.data directly first
323
- const passed = (rest as any).data;
324
- if (Array.isArray(passed)) {
325
- setData(passed);
326
- setLoading(false);
327
- return;
328
- }
329
- }
330
-
331
- // Check schema.data next
332
- if ((schema as any).data) {
333
- const passed = (schema as any).data;
334
- if (Array.isArray(passed)) {
335
- setData(passed);
336
- setLoading(false);
337
- return;
338
- }
339
- }
340
-
341
- if (hasInlineData && dataConfig?.provider === 'value') {
342
- setData(dataConfig.items as any[]);
343
- setLoading(false);
344
- return;
345
- }
346
-
347
- if (!dataSource || typeof dataSource.find !== 'function') {
348
- throw new Error('DataSource required for object/api providers');
349
- }
350
-
351
- if (dataConfig?.provider === 'object') {
352
- const objectName = dataConfig.object;
353
- // Auto-inject $expand for lookup/master_detail fields
354
- const expand = buildExpandFields(objectSchema?.fields);
355
- const result = await dataSource.find(objectName, {
356
- $filter: schema.filter,
357
- $orderby: convertSortToQueryParams(schema.sort),
358
- ...(expand.length > 0 ? { $expand: expand } : {}),
359
- });
360
-
361
- let items: any[] = extractRecords(result);
362
- setData(items);
363
- } else if (dataConfig?.provider === 'api') {
364
- console.warn('API provider not yet implemented for ObjectMap');
365
- setData([]);
366
- }
367
-
368
- setLoading(false);
369
- } catch (err) {
370
- setError(err as Error);
371
- setLoading(false);
372
- }
373
- };
374
-
375
- fetchData();
376
- }, [dataConfig, dataSource, hasInlineData, schema.filter, schema.sort, objectSchema]);
377
-
378
- // Fetch object schema for field metadata
379
- useEffect(() => {
380
- const fetchObjectSchema = async () => {
381
- try {
382
- if (!dataSource) return;
383
-
384
- const objectName = dataConfig?.provider === 'object'
385
- ? dataConfig.object
386
- : schema.objectName;
387
-
388
- if (!objectName) return;
389
-
390
- const schemaData = await dataSource.getObjectSchema(objectName);
391
- setObjectSchema(schemaData);
392
- } catch (err) {
393
- console.error('Failed to fetch object schema:', err);
394
- }
395
- };
396
-
397
- if (!hasInlineData && dataSource) {
398
- fetchObjectSchema();
399
- }
400
- }, [schema.objectName, dataSource, hasInlineData, dataConfig]);
401
-
402
- // Transform data to map markers
403
- const { markers, invalidCount } = useMemo(() => {
404
- let invalid = 0;
405
- const validMarkers = data
406
- .map((record, index) => {
407
- const coordinates = extractCoordinates(record, mapConfig);
408
- if (!coordinates) {
409
- invalid++;
410
- return null;
411
- }
412
-
413
- const title = mapConfig.titleField ? record[mapConfig.titleField] : 'Marker';
414
- const description = mapConfig.descriptionField ? record[mapConfig.descriptionField] : undefined;
415
-
416
- // Ensure lat/lng are within valid ranges
417
- const [lat, lng] = coordinates;
418
- if (!isFinite(lat) || !isFinite(lng) || lat < -90 || lat > 90 || lng < -180 || lng > 180) {
419
- invalid++;
420
- return null;
421
- }
422
-
423
- return {
424
- id: record.id || record._id || `marker-${index}`,
425
- title,
426
- description,
427
- coordinates: [lng, lat] as [number, number], // maplibre uses [lng, lat]
428
- data: record,
429
- };
430
- })
431
- .filter((marker): marker is NonNullable<typeof marker> => marker !== null);
432
-
433
- return { markers: validMarkers, invalidCount: invalid };
434
- }, [data, mapConfig]);
435
-
436
- const selectedMarker = useMemo(() =>
437
- markers.find(m => m.id === selectedMarkerId),
438
- [markers, selectedMarkerId]);
439
-
440
- const [currentZoom, setCurrentZoom] = useState(mapConfig.zoom || 3);
441
-
442
- const navigation = useNavigationOverlay({
443
- navigation: (schema as any).navigation,
444
- objectName: schema.objectName,
445
- onRowClick,
446
- });
447
-
448
- const filteredMarkers = useMemo(() => {
449
- if (!searchQuery.trim()) return markers;
450
- const q = searchQuery.toLowerCase();
451
- return markers.filter(m =>
452
- m.title?.toLowerCase().includes(q) ||
453
- m.description?.toLowerCase().includes(q)
454
- );
455
- }, [markers, searchQuery]);
456
-
457
- // Cluster markers when clustering is enabled
458
- const clusteredData = useMemo(() => {
459
- const shouldCluster = enableClustering ?? ((schema as any).enableClustering || filteredMarkers.length > 100);
460
- if (!shouldCluster) {
461
- return filteredMarkers.map(m => ({
462
- id: m.id,
463
- coordinates: m.coordinates,
464
- markers: [m],
465
- isCluster: false,
466
- }));
467
- }
468
- return clusterMarkers(filteredMarkers, currentZoom, clusterRadius);
469
- }, [filteredMarkers, currentZoom, enableClustering, clusterRadius, schema]);
470
-
471
- // Calculate map bounds
472
- const initialViewState = useMemo(() => {
473
- if (!filteredMarkers.length) {
474
- return {
475
- longitude: mapConfig.center?.[1] || 0,
476
- latitude: mapConfig.center?.[0] || 0,
477
- zoom: mapConfig.zoom || 2
478
- };
479
- }
480
-
481
- // Simple bounds calculation
482
- const lngs = filteredMarkers.map(m => m.coordinates[0]);
483
- const lats = filteredMarkers.map(m => m.coordinates[1]);
484
-
485
- const minLng = Math.min(...lngs);
486
- const maxLng = Math.max(...lngs);
487
- const minLat = Math.min(...lats);
488
- const maxLat = Math.max(...lats);
489
-
490
- return {
491
- longitude: (minLng + maxLng) / 2,
492
- latitude: (minLat + maxLat) / 2,
493
- zoom: mapConfig.zoom || 3, // Auto-zoom logic could be improved here
494
- };
495
- }, [filteredMarkers, mapConfig]);
496
-
497
- if (loading) {
498
- return (
499
- <div className={cn("min-w-0 overflow-hidden", className)}>
500
- <div className="flex items-center justify-center h-96 bg-muted rounded-lg border">
501
- <div className="text-muted-foreground">Loading map...</div>
502
- </div>
503
- </div>
504
- );
505
- }
506
-
507
- if (error) {
508
- return (
509
- <div className={cn("min-w-0 overflow-hidden", className)}>
510
- <div className="flex items-center justify-center h-96 bg-muted rounded-lg border">
511
- <div className="text-destructive">Error: {error.message}</div>
512
- </div>
513
- </div>
514
- );
515
- }
516
-
517
- return (
518
- <div className={cn("min-w-0 overflow-hidden", className)}>
519
- {invalidCount > 0 && (
520
- <div className="mb-2 p-2 text-sm text-yellow-800 bg-yellow-50 border border-yellow-200 rounded">
521
- {`${invalidCount} record${invalidCount !== 1 ? 's' : ''} with missing or invalid coordinates excluded from the map.`}
522
- </div>
523
- )}
524
- {markers.length > 0 && (
525
- <div className="mb-2">
526
- <input
527
- type="text"
528
- value={searchQuery}
529
- onChange={(e) => setSearchQuery(e.target.value)}
530
- placeholder="Search locations…"
531
- className="w-full px-3 py-2 text-sm border rounded-md bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
532
- />
533
- </div>
534
- )}
535
- <div className="relative border rounded-lg overflow-hidden bg-muted h-[300px] sm:h-[400px] md:h-[500px] lg:h-[600px] w-full">
536
- <MapGL
537
- initialViewState={initialViewState}
538
- style={{ width: '100%', height: '100%' }}
539
- mapStyle="https://demotiles.maplibre.org/style.json"
540
- touchZoomRotate={true}
541
- dragRotate={true}
542
- touchPitch={true}
543
- onZoom={(e) => setCurrentZoom(Math.round(e.viewState.zoom))}
544
- >
545
- <NavigationControl position="top-right" showCompass={true} showZoom={true} />
546
-
547
- {clusteredData.map(cluster => (
548
- cluster.isCluster ? (
549
- <Marker
550
- key={cluster.id}
551
- longitude={cluster.coordinates[0]}
552
- latitude={cluster.coordinates[1]}
553
- anchor="center"
554
- >
555
- <div
556
- className="flex items-center justify-center rounded-full bg-primary text-primary-foreground font-bold text-xs cursor-pointer hover:scale-110 transition-transform shadow-md"
557
- style={{
558
- width: Math.min(48, 24 + cluster.markers.length * 2),
559
- height: Math.min(48, 24 + cluster.markers.length * 2),
560
- }}
561
- title={`${cluster.markers.length} markers`}
562
- >
563
- {cluster.markers.length}
564
- </div>
565
- </Marker>
566
- ) : (
567
- <Marker
568
- key={cluster.id}
569
- longitude={cluster.coordinates[0]}
570
- latitude={cluster.coordinates[1]}
571
- anchor="bottom"
572
- onClick={(e) => {
573
- e.originalEvent.stopPropagation();
574
- const marker = cluster.markers[0];
575
- setSelectedMarkerId(marker.id);
576
- navigation.handleClick(marker.data);
577
- onMarkerClick?.(marker.data);
578
- }}
579
- >
580
- <div className="text-2xl cursor-pointer hover:scale-110 transition-transform">
581
- 📍
582
- </div>
583
- </Marker>
584
- )
585
- ))}
586
-
587
- {selectedMarker && (
588
- <Popup
589
- longitude={selectedMarker.coordinates[0]}
590
- latitude={selectedMarker.coordinates[1]}
591
- anchor="top"
592
- onClose={() => setSelectedMarkerId(null)}
593
- closeOnClick={false}
594
- >
595
- <div className="p-2 min-w-[150px] sm:min-w-[200px]">
596
- <h3 className="font-bold text-sm mb-1">{selectedMarker.title}</h3>
597
- {selectedMarker.description && (
598
- <p className="text-xs text-muted-foreground">{selectedMarker.description}</p>
599
- )}
600
- <div className="mt-2 text-xs flex gap-2">
601
- {onEdit && (
602
- <button className="text-blue-500 hover:underline" onClick={() => onEdit(selectedMarker.data)}>Edit</button>
603
- )}
604
- {onDelete && (
605
- <button className="text-red-500 hover:underline" onClick={() => onDelete(selectedMarker.data)}>Delete</button>
606
- )}
607
- </div>
608
- </div>
609
- </Popup>
610
- )}
611
- </MapGL>
612
- </div>
613
- {navigation.isOverlay && (
614
- <NavigationOverlay {...navigation} title="Location Details">
615
- {(record) => (
616
- <div className="space-y-3">
617
- {Object.entries(record).map(([key, value]) => (
618
- <div key={key} className="flex flex-col">
619
- <span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
620
- {key.replace(/_/g, ' ')}
621
- </span>
622
- <span className="text-sm">{String(value ?? '—')}</span>
623
- </div>
624
- ))}
625
- </div>
626
- )}
627
- </NavigationOverlay>
628
- )}
629
- </div>
630
- );
631
- };
632
-
633
- export default ObjectMap;
@@ -1,27 +0,0 @@
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 DELETED
@@ -1,43 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import React from 'react';
10
- import { ComponentRegistry } from '@object-ui/core';
11
- import { useSchemaContext } from '@object-ui/react';
12
- import { ObjectMap } from './ObjectMap';
13
- import type { ObjectMapProps } from './ObjectMap';
14
-
15
- export { ObjectMap };
16
- export type { ObjectMapProps };
17
-
18
- // Register component
19
- export const ObjectMapRenderer: React.FC<any> = ({ schema, ...props }) => {
20
- const { dataSource } = useSchemaContext() || {};
21
- return <ObjectMap schema={schema} dataSource={dataSource} {...props} />;
22
- };
23
-
24
- console.log('Registering object-map...');
25
- ComponentRegistry.register('object-map', ObjectMapRenderer, {
26
- namespace: 'plugin-map',
27
- label: 'Object Map',
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',
39
- inputs: [
40
- { name: 'objectName', type: 'string', label: 'Object Name', required: true },
41
- { name: 'map', type: 'object', label: 'Map Config', description: 'latitudeField, longitudeField, titleField' },
42
- ],
43
- });