@smartnet360/svelte-components 0.0.144 → 0.0.147

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.
@@ -110,11 +110,14 @@
110
110
  if (externalAntenna1) {
111
111
  const found = findAntennaBySpec(antennas, externalAntenna1);
112
112
  if (found) {
113
+ // Compute available tilts INLINE to avoid stale state issue
114
+ const tiltsForAntenna1 = getAvailableTiltsForAntenna(found);
115
+
113
116
  // Use untrack to prevent infinite loop from state updates
114
117
  untrack(() => {
115
118
  selectedAntenna = found;
116
- updateAvailableTilts(found);
117
- ant1ElectricalTilt = findTiltIndex(availableElectricalTilts, externalAntenna1.electricalTilt ?? 0);
119
+ availableElectricalTilts = tiltsForAntenna1;
120
+ ant1ElectricalTilt = findTiltIndex(tiltsForAntenna1, externalAntenna1.electricalTilt ?? 0);
118
121
  ant1MechanicalTilt = externalAntenna1.mechanicalTilt ?? 0;
119
122
  });
120
123
  } else {
@@ -126,9 +129,10 @@
126
129
  if (externalAntenna2) {
127
130
  const found2 = findAntennaBySpec(antennas, externalAntenna2);
128
131
  if (found2) {
132
+ const tiltsForAntenna2 = getAvailableTiltsForAntenna(found2);
129
133
  untrack(() => {
130
134
  selectedAntenna2 = found2;
131
- ant2ElectricalTilt = findTiltIndex(getAvailableTiltsForAntenna(found2), externalAntenna2.electricalTilt ?? 0);
135
+ ant2ElectricalTilt = findTiltIndex(tiltsForAntenna2, externalAntenna2.electricalTilt ?? 0);
132
136
  ant2MechanicalTilt = externalAntenna2.mechanicalTilt ?? 0;
133
137
  if (viewMode === 'single') {
134
138
  viewMode = 'compare';
@@ -71,6 +71,8 @@
71
71
  let csvHeaders = $state<string[]>([]);
72
72
  let columnMapping = $state<ColumnMapping>({});
73
73
  let importMode = $state<ImportMode>('cell');
74
+ let includeAllExtras = $state(true);
75
+ let selectedExtras = $state<number[]>([]);
74
76
 
75
77
  // Quick Add state
76
78
  let showQuickAdd = $state(false);
@@ -203,7 +205,11 @@
203
205
  if (!pendingCsvContent) return;
204
206
 
205
207
  try {
206
- importResult = setsStore.importFromCsv(pendingCsvContent, importFileName, columnMapping);
208
+ importResult = setsStore.importFromCsv(pendingCsvContent, importFileName, {
209
+ columnMapping,
210
+ includeAllExtras,
211
+ selectedExtraIndices: selectedExtras
212
+ });
207
213
  showMappingModal = false;
208
214
  showImportModal = true;
209
215
  } catch (err) {
@@ -219,6 +225,8 @@
219
225
  csvHeaders = [];
220
226
  columnMapping = {};
221
227
  importMode = 'cell';
228
+ includeAllExtras = true;
229
+ selectedExtras = [];
222
230
  }
223
231
 
224
232
  function confirmImport() {
@@ -486,6 +494,7 @@
486
494
  {#each setsStore.sets as set (set.id)}
487
495
  {@const isExpanded = expandedSets.has(set.id)}
488
496
  {@const treeStore = treeStores.get(set.id)}
497
+ {@const hasPoints = set.cells.some(c => c.geometry === 'point')}
489
498
  <div class="set-section" class:expanded={isExpanded}>
490
499
  <!-- Set Header -->
491
500
  <div class="set-header d-flex align-items-center justify-content-between px-2 py-2">
@@ -504,6 +513,15 @@
504
513
  >
505
514
  <i class="bi bi-eye{set.visible ? '-fill text-primary' : '-slash text-muted'}"></i>
506
515
  </button>
516
+ <button
517
+ class="btn btn-sm p-0 me-2"
518
+ title={!hasPoints ? 'No points to label' : set.showLabels ? 'Hide Labels' : 'Show Labels'}
519
+ aria-label={!hasPoints ? 'No points to label' : set.showLabels ? 'Hide Labels' : 'Show Labels'}
520
+ onclick={() => setsStore.toggleSetLabels(set.id)}
521
+ disabled={!hasPoints}
522
+ >
523
+ <i class="bi bi-tag{!hasPoints ? ' text-muted opacity-50' : set.showLabels ? '-fill text-primary' : ' text-muted'}"></i>
524
+ </button>
507
525
  <div class="set-info text-truncate" onclick={() => toggleExpanded(set.id)} style="cursor: pointer;">
508
526
  <div class="fw-medium small text-truncate">{set.name}</div>
509
527
  <small class="text-muted">{getSetItemCounts(set)} · {set.groups.length} groups</small>
@@ -596,8 +614,14 @@
596
614
  headers={csvHeaders}
597
615
  mode={importMode}
598
616
  mapping={columnMapping}
617
+ {includeAllExtras}
618
+ {selectedExtras}
599
619
  onmodechange={(m) => importMode = m}
600
620
  onchange={(m) => columnMapping = m}
621
+ onextraschange={(all, selected) => {
622
+ includeAllExtras = all;
623
+ selectedExtras = selected;
624
+ }}
601
625
  />
602
626
  </div>
603
627
  <div class="modal-footer py-2">
@@ -51,11 +51,13 @@
51
51
  cellFill: string;
52
52
  cellLine: string;
53
53
  pointCircle: string;
54
+ pointLabel: string;
54
55
  } {
55
56
  return {
56
57
  cellFill: `custom-cells-fill-${setId}`,
57
58
  cellLine: `custom-cells-line-${setId}`,
58
- pointCircle: `custom-points-circle-${setId}`
59
+ pointCircle: `custom-points-circle-${setId}`,
60
+ pointLabel: `custom-points-label-${setId}`
59
61
  };
60
62
  }
61
63
 
@@ -129,6 +131,30 @@
129
131
  });
130
132
  activeLayers.add(layers.pointCircle);
131
133
  }
134
+
135
+ // Add points label layer (symbol layer for text)
136
+ if (!map.getLayer(layers.pointLabel)) {
137
+ map.addLayer({
138
+ id: layers.pointLabel,
139
+ type: 'symbol',
140
+ source: sources.points,
141
+ layout: {
142
+ 'text-field': ['get', 'labelText'],
143
+ 'text-size': 11,
144
+ 'text-anchor': 'top',
145
+ 'text-offset': [0, 0.8],
146
+ 'text-allow-overlap': false,
147
+ 'text-ignore-placement': false,
148
+ 'visibility': set.showLabels ? 'visible' : 'none'
149
+ },
150
+ paint: {
151
+ 'text-color': '#333',
152
+ 'text-halo-color': '#fff',
153
+ 'text-halo-width': 1.5
154
+ }
155
+ });
156
+ activeLayers.add(layers.pointLabel);
157
+ }
132
158
  }
133
159
 
134
160
  /**
@@ -138,8 +164,8 @@
138
164
  const sources = getSourceIds(setIdToRemove);
139
165
  const layers = getLayerIds(setIdToRemove);
140
166
 
141
- // Remove layers
142
- for (const layerId of [layers.cellLine, layers.cellFill, layers.pointCircle]) {
167
+ // Remove layers (label layer must be removed first since it's on top)
168
+ for (const layerId of [layers.pointLabel, layers.cellLine, layers.cellFill, layers.pointCircle]) {
143
169
  if (map.getLayer(layerId)) {
144
170
  map.removeLayer(layerId);
145
171
  activeLayers.delete(layerId);
@@ -189,6 +215,9 @@
189
215
  // Darken color for stroke
190
216
  const strokeColor = darkenColor(color, 0.3);
191
217
 
218
+ // Generate label text from first 3 extra fields
219
+ const labelText = buildLabelText(item.extraFields);
220
+
192
221
  pointFeatures.push({
193
222
  type: 'Feature',
194
223
  geometry: {
@@ -203,9 +232,8 @@
203
232
  customGroup: item.customGroup,
204
233
  sizeFactor: item.sizeFactor,
205
234
  setName: set.name,
206
- ...Object.fromEntries(
207
- Object.entries(item.extraFields).map(([k, v]) => [`extra_${k}`, v])
208
- )
235
+ popupInfo: { ...item.extraFields },
236
+ labelText
209
237
  }
210
238
  });
211
239
  } else if (item.geometry === 'cell' && item.resolvedCell) {
@@ -226,11 +254,7 @@
226
254
  feature.properties.customGroup = item.customGroup;
227
255
  feature.properties.sizeFactor = item.sizeFactor;
228
256
  feature.properties.setName = set.name;
229
-
230
- // Add extra fields
231
- for (const [key, value] of Object.entries(item.extraFields)) {
232
- feature.properties[`extra_${key}`] = value;
233
- }
257
+ feature.properties.popupInfo = { ...item.extraFields };
234
258
  }
235
259
 
236
260
  cellFeatures.push(feature);
@@ -265,9 +289,23 @@
265
289
  map.setPaintProperty(layers.pointCircle, 'circle-opacity', set.opacity);
266
290
  }
267
291
 
292
+ // Update label visibility
293
+ if (map.getLayer(layers.pointLabel)) {
294
+ map.setLayoutProperty(layers.pointLabel, 'visibility', set.showLabels ? 'visible' : 'none');
295
+ }
296
+
268
297
  console.log(`[CustomLayer] Rendered ${cellFeatures.length} cells, ${pointFeatures.length} points for set "${set.name}"`);
269
298
  }
270
299
 
300
+ /**
301
+ * Build label text from extra fields (first 3 values, newline separated)
302
+ */
303
+ function buildLabelText(extraFields: Record<string, string | number> | undefined): string {
304
+ if (!extraFields) return '';
305
+ const values = Object.values(extraFields).slice(0, 3);
306
+ return values.filter(v => v !== undefined && v !== null && v !== '').join('\n');
307
+ }
308
+
271
309
  /**
272
310
  * Darken a hex color by a factor
273
311
  */
@@ -26,15 +26,27 @@ export type CsvDelimiter = ',' | ';' | 'auto';
26
26
  * @returns Array of header strings
27
27
  */
28
28
  export declare function extractCsvHeaders(csvContent: string, delimiter?: CsvDelimiter): string[];
29
+ /**
30
+ * Options for CSV parsing
31
+ */
32
+ export interface CsvParseOptions {
33
+ /** Delimiter to use: ',' (comma), ';' (semicolon), or 'auto' (detect) */
34
+ delimiter?: CsvDelimiter;
35
+ /** User-defined column mapping (overrides auto-detection) */
36
+ columnMapping?: ColumnMapping;
37
+ /** Whether to include all extra columns (default: true) */
38
+ includeAllExtras?: boolean;
39
+ /** Specific extra column indices to include (when includeAllExtras is false) */
40
+ selectedExtraIndices?: number[];
41
+ }
29
42
  /**
30
43
  * Parse a CSV string into custom items (cells or points)
31
44
  * @param csvContent Raw CSV content
32
45
  * @param cellLookup Map of cellName -> Cell for resolving cell data
33
- * @param delimiter Delimiter to use: ',' (comma), ';' (semicolon), or 'auto' (detect)
34
- * @param columnMapping Optional user-defined column mapping (overrides auto-detection)
46
+ * @param options Parsing options
35
47
  * @returns Import result with items, unmatched IDs, groups, and extra columns
36
48
  */
37
- export declare function parseCustomCellsCsv(csvContent: string, cellLookup: Map<string, Cell>, delimiter?: CsvDelimiter, columnMapping?: ColumnMapping): CustomCellImportResult;
49
+ export declare function parseCustomCellsCsv(csvContent: string, cellLookup: Map<string, Cell>, options?: CsvParseOptions): CustomCellImportResult;
38
50
  /**
39
51
  * Build a cell lookup map from an array of cells
40
52
  * Creates lookups for both cellName and txId for flexible matching
@@ -49,11 +49,11 @@ export function extractCsvHeaders(csvContent, delimiter = 'auto') {
49
49
  * Parse a CSV string into custom items (cells or points)
50
50
  * @param csvContent Raw CSV content
51
51
  * @param cellLookup Map of cellName -> Cell for resolving cell data
52
- * @param delimiter Delimiter to use: ',' (comma), ';' (semicolon), or 'auto' (detect)
53
- * @param columnMapping Optional user-defined column mapping (overrides auto-detection)
52
+ * @param options Parsing options
54
53
  * @returns Import result with items, unmatched IDs, groups, and extra columns
55
54
  */
56
- export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto', columnMapping) {
55
+ export function parseCustomCellsCsv(csvContent, cellLookup, options = {}) {
56
+ const { delimiter = 'auto', columnMapping, includeAllExtras = true, selectedExtraIndices = [] } = options;
57
57
  const lines = csvContent.trim().split('\n');
58
58
  if (lines.length < 2) {
59
59
  return {
@@ -106,30 +106,55 @@ export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto',
106
106
  throw new Error('CSV must contain an "id", "cellName", or "txId" column');
107
107
  }
108
108
  const hasLatLon = latIndex !== -1 && lonIndex !== -1;
109
- // Find extra columns (not reserved)
109
+ // Find extra columns based on user selection
110
110
  const extraColumns = [];
111
111
  const extraIndices = [];
112
- headers.forEach((header, idx) => {
113
- const normalized = normalizeColumnName(header);
114
- if (!RESERVED_COLUMNS.includes(normalized)) {
115
- extraColumns.push(header.trim());
116
- extraIndices.push(idx);
112
+ // Get indices that are already mapped to known fields
113
+ const mappedIndices = new Set([idIndex, groupIndex, sizeFactorIndex, latIndex, lonIndex].filter(i => i >= 0));
114
+ if (includeAllExtras) {
115
+ // Include all non-reserved, non-mapped columns
116
+ headers.forEach((header, idx) => {
117
+ if (mappedIndices.has(idx))
118
+ return;
119
+ const normalized = normalizeColumnName(header);
120
+ if (!RESERVED_COLUMNS.includes(normalized)) {
121
+ extraColumns.push(header.trim());
122
+ extraIndices.push(idx);
123
+ }
124
+ });
125
+ }
126
+ else {
127
+ // Include only specifically selected columns
128
+ for (const idx of selectedExtraIndices) {
129
+ if (idx >= 0 && idx < headers.length && !mappedIndices.has(idx)) {
130
+ extraColumns.push(headers[idx].trim());
131
+ extraIndices.push(idx);
132
+ }
117
133
  }
118
- });
134
+ }
119
135
  // Parse data rows
120
136
  const cells = [];
121
137
  const unmatchedTxIds = [];
122
138
  const groupsSet = new Set();
123
139
  let cellCount = 0;
124
140
  let pointCount = 0;
141
+ let autoIdCounter = 0;
125
142
  for (let i = 1; i < lines.length; i++) {
126
143
  const line = lines[i].trim();
127
144
  if (!line)
128
145
  continue;
129
146
  const values = parseCSVLine(line, actualDelimiter);
130
- const itemId = values[idIndex]?.trim();
131
- if (!itemId)
132
- continue;
147
+ // Get item ID - auto-generate for points if no ID column mapped
148
+ let itemId;
149
+ if (idIndex >= 0) {
150
+ itemId = values[idIndex]?.trim() || '';
151
+ if (!itemId)
152
+ continue; // Skip rows with empty ID when ID column is mapped
153
+ }
154
+ else {
155
+ // No ID column mapped - will auto-generate for points
156
+ itemId = '';
157
+ }
133
158
  // Get custom group (default to 'default')
134
159
  const customGroup = groupIndex !== -1
135
160
  ? (values[groupIndex]?.trim() || 'default')
@@ -171,8 +196,10 @@ export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto',
171
196
  lat = parsedLat;
172
197
  lon = parsedLon;
173
198
  pointCount++;
199
+ // Auto-generate ID for points if not mapped
200
+ const pointId = itemId || `Point ${++autoIdCounter}`;
174
201
  cells.push({
175
- id: itemId,
202
+ id: pointId,
176
203
  customGroup,
177
204
  sizeFactor,
178
205
  extraFields,
@@ -183,7 +210,9 @@ export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto',
183
210
  continue;
184
211
  }
185
212
  }
186
- // No valid lat/lon - try to resolve as cell
213
+ // No valid lat/lon - try to resolve as cell (requires ID)
214
+ if (!itemId)
215
+ continue; // Can't resolve cell without ID
187
216
  resolvedCell = cellLookup.get(itemId);
188
217
  if (resolvedCell) {
189
218
  cellCount++;
@@ -7,7 +7,7 @@
7
7
  import type { Cell } from '../../../../shared/demo';
8
8
  import type { CellDataStore } from '../../cells/stores/cell.data.svelte';
9
9
  import type { CustomCellSet, CustomCellImportResult } from '../types';
10
- import type { ColumnMapping } from '../../../../shared/csv-import';
10
+ import { type CsvParseOptions } from '../logic/csv-parser';
11
11
  /** Function that returns the current cells array */
12
12
  type CellsGetter = () => Cell[];
13
13
  /**
@@ -32,9 +32,9 @@ export declare class CustomCellSetsStore {
32
32
  * Import a CSV file and create a new custom cell set
33
33
  * @param csvContent Raw CSV content
34
34
  * @param fileName Name of the file being imported
35
- * @param columnMapping Optional user-defined column mapping
35
+ * @param options Parsing options (column mapping, extra fields selection)
36
36
  */
37
- importFromCsv(csvContent: string, fileName: string, columnMapping?: ColumnMapping): CustomCellImportResult;
37
+ importFromCsv(csvContent: string, fileName: string, options?: CsvParseOptions): CustomCellImportResult;
38
38
  /**
39
39
  * Create a new set from import result
40
40
  */
@@ -51,6 +51,10 @@ export declare class CustomCellSetsStore {
51
51
  * Toggle set visibility
52
52
  */
53
53
  toggleSetVisibility(setId: string): void;
54
+ /**
55
+ * Toggle set labels visibility
56
+ */
57
+ toggleSetLabels(setId: string): void;
54
58
  /**
55
59
  * Toggle group visibility within a set
56
60
  */
@@ -69,13 +69,13 @@ export class CustomCellSetsStore {
69
69
  * Import a CSV file and create a new custom cell set
70
70
  * @param csvContent Raw CSV content
71
71
  * @param fileName Name of the file being imported
72
- * @param columnMapping Optional user-defined column mapping
72
+ * @param options Parsing options (column mapping, extra fields selection)
73
73
  */
74
- importFromCsv(csvContent, fileName, columnMapping) {
74
+ importFromCsv(csvContent, fileName, options) {
75
75
  // Build lookup from all cells
76
76
  const cellLookup = buildCellLookup(this.getCells());
77
- // Parse CSV with optional user-defined column mapping
78
- const result = parseCustomCellsCsv(csvContent, cellLookup, 'auto', columnMapping);
77
+ // Parse CSV with options
78
+ const result = parseCustomCellsCsv(csvContent, cellLookup, options || {});
79
79
  return result;
80
80
  }
81
81
  /**
@@ -101,6 +101,7 @@ export class CustomCellSetsStore {
101
101
  defaultColor: CUSTOM_CELL_PALETTE[0],
102
102
  groupColors,
103
103
  visible: true,
104
+ showLabels: false,
104
105
  visibleGroups: new Set(importResult.groups)
105
106
  };
106
107
  this.sets.push(newSet);
@@ -136,6 +137,16 @@ export class CustomCellSetsStore {
136
137
  this.version++;
137
138
  }
138
139
  }
140
+ /**
141
+ * Toggle set labels visibility
142
+ */
143
+ toggleSetLabels(setId) {
144
+ const set = this.getSet(setId);
145
+ if (set) {
146
+ set.showLabels = !set.showLabels;
147
+ this.version++;
148
+ }
149
+ }
139
150
  /**
140
151
  * Toggle group visibility within a set
141
152
  */
@@ -227,6 +238,7 @@ export class CustomCellSetsStore {
227
238
  this.sets = data.map((setData) => ({
228
239
  ...setData,
229
240
  visibleGroups: new Set(setData.visibleGroups || []),
241
+ showLabels: setData.showLabels ?? false,
230
242
  cells: setData.cells || []
231
243
  }));
232
244
  // Re-resolve cells
@@ -81,6 +81,8 @@ export interface CustomCellSet {
81
81
  groupColors: Record<string, string>;
82
82
  /** Whether this set's layer is visible */
83
83
  visible: boolean;
84
+ /** Whether to show labels for points */
85
+ showLabels: boolean;
84
86
  /** Which groups are currently visible */
85
87
  visibleGroups: Set<string>;
86
88
  }
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * Compact UI for mapping CSV columns to expected fields.
6
6
  * Includes import mode selection (Cell/Point/Site) with auto-detection.
7
+ * Supports extra field selection for tooltips.
7
8
  */
8
9
  import type { ColumnMapping, ImportFieldsConfig, ImportMode } from './types';
9
10
  import { getFieldsForMode } from './types';
@@ -16,18 +17,27 @@
16
17
  mode: ImportMode;
17
18
  /** Current mapping (can be auto-detected or user-modified) */
18
19
  mapping: ColumnMapping;
20
+ /** Whether to include all extra columns */
21
+ includeAllExtras?: boolean;
22
+ /** Selected extra column indices (when not including all) */
23
+ selectedExtras?: number[];
19
24
  /** Called when mode changes */
20
25
  onmodechange?: (mode: ImportMode) => void;
21
26
  /** Called when mapping changes */
22
27
  onchange?: (mapping: ColumnMapping) => void;
28
+ /** Called when extra fields settings change */
29
+ onextraschange?: (includeAll: boolean, selected: number[]) => void;
23
30
  }
24
31
 
25
32
  let {
26
33
  headers,
27
34
  mode,
28
35
  mapping,
36
+ includeAllExtras = true,
37
+ selectedExtras = [],
29
38
  onmodechange,
30
- onchange
39
+ onchange,
40
+ onextraschange
31
41
  }: Props = $props();
32
42
 
33
43
  // Get fields config based on current mode
@@ -36,6 +46,18 @@
36
46
  // All fields to display
37
47
  let allFields = $derived([...fieldsConfig.required, ...fieldsConfig.optional]);
38
48
 
49
+ // Get mapped column indices (to exclude from extra fields options)
50
+ let mappedIndices = $derived(new Set(
51
+ Object.values(mapping).filter((v): v is number => v !== null && v !== undefined)
52
+ ));
53
+
54
+ // Available columns for extra fields (not already mapped)
55
+ let availableExtras = $derived(
56
+ headers
57
+ .map((header, idx) => ({ header, idx }))
58
+ .filter(({ idx }) => !mappedIndices.has(idx))
59
+ );
60
+
39
61
  // Validation state
40
62
  let missingRequired = $derived(
41
63
  fieldsConfig.required.filter(f =>
@@ -78,6 +100,35 @@
78
100
  const idx = mapping[fieldType];
79
101
  return idx !== null && idx !== undefined ? headers[idx] : null;
80
102
  }
103
+
104
+ // Extra fields handlers
105
+ function handleIncludeAllChange(event: Event) {
106
+ const checked = (event.target as HTMLInputElement).checked;
107
+ onextraschange?.(checked, selectedExtras);
108
+ }
109
+
110
+ function handleExtraFieldChange(slotIndex: number, event: Event) {
111
+ const select = event.target as HTMLSelectElement;
112
+ const value = select.value;
113
+
114
+ const newSelected = [...selectedExtras];
115
+
116
+ if (value === '') {
117
+ // Remove this slot
118
+ newSelected[slotIndex] = -1;
119
+ } else {
120
+ newSelected[slotIndex] = parseInt(value, 10);
121
+ }
122
+
123
+ // Filter out -1 values and duplicates
124
+ const filtered = newSelected.filter((v, i) => v >= 0 && newSelected.indexOf(v) === i);
125
+ onextraschange?.(includeAllExtras, filtered);
126
+ }
127
+
128
+ function getExtraSlotValue(slotIndex: number): string {
129
+ const value = selectedExtras[slotIndex];
130
+ return value === undefined || value < 0 ? '' : String(value);
131
+ }
81
132
  </script>
82
133
 
83
134
  <div class="column-mapper">
@@ -150,13 +201,56 @@
150
201
  >
151
202
  <option value="">{field.description || '-- Select --'}</option>
152
203
  {#each headers as header, idx}
153
- <option value={idx}>{header}</option>
204
+ <option value={String(idx)}>{header}</option>
154
205
  {/each}
155
206
  </select>
156
207
  </div>
157
208
  {/each}
158
209
  </div>
159
210
 
211
+ <!-- Extra Fields Section -->
212
+ {#if availableExtras.length > 0}
213
+ <div class="extra-fields mt-3 pt-2 border-top">
214
+ <div class="d-flex align-items-center justify-content-between mb-2">
215
+ <span class="small fw-medium">Extra columns for popups</span>
216
+ <div class="form-check form-check-inline mb-0">
217
+ <input
218
+ class="form-check-input"
219
+ type="checkbox"
220
+ id="include-all-extras"
221
+ checked={includeAllExtras}
222
+ onchange={handleIncludeAllChange}
223
+ />
224
+ <label class="form-check-label small" for="include-all-extras">
225
+ Include all ({availableExtras.length})
226
+ </label>
227
+ </div>
228
+ </div>
229
+
230
+ {#if !includeAllExtras}
231
+ <div class="extra-slots">
232
+ {#each [0, 1, 2] as slotIndex}
233
+ {@const slotId = `extra-slot-${slotIndex}`}
234
+ <div class="d-flex align-items-center gap-2 mb-1">
235
+ <select
236
+ id={slotId}
237
+ class="form-select form-select-sm"
238
+ value={getExtraSlotValue(slotIndex)}
239
+ onchange={(e) => handleExtraFieldChange(slotIndex, e)}
240
+ aria-label={`Extra field ${slotIndex + 1}`}
241
+ >
242
+ <option value="">-- Extra {slotIndex + 1} --</option>
243
+ {#each availableExtras as { header, idx }}
244
+ <option value={String(idx)}>{header}</option>
245
+ {/each}
246
+ </select>
247
+ </div>
248
+ {/each}
249
+ </div>
250
+ {/if}
251
+ </div>
252
+ {/if}
253
+
160
254
  <!-- Validation Warning -->
161
255
  {#if missingRequired.length > 0}
162
256
  <div class="alert alert-warning py-1 px-2 mt-2 mb-0 small">
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Compact UI for mapping CSV columns to expected fields.
5
5
  * Includes import mode selection (Cell/Point/Site) with auto-detection.
6
+ * Supports extra field selection for tooltips.
6
7
  */
7
8
  import type { ColumnMapping, ImportMode } from './types';
8
9
  interface Props {
@@ -12,10 +13,16 @@ interface Props {
12
13
  mode: ImportMode;
13
14
  /** Current mapping (can be auto-detected or user-modified) */
14
15
  mapping: ColumnMapping;
16
+ /** Whether to include all extra columns */
17
+ includeAllExtras?: boolean;
18
+ /** Selected extra column indices (when not including all) */
19
+ selectedExtras?: number[];
15
20
  /** Called when mode changes */
16
21
  onmodechange?: (mode: ImportMode) => void;
17
22
  /** Called when mapping changes */
18
23
  onchange?: (mapping: ColumnMapping) => void;
24
+ /** Called when extra fields settings change */
25
+ onextraschange?: (includeAll: boolean, selected: number[]) => void;
19
26
  }
20
27
  declare const ColumnMapper: import("svelte").Component<Props, {}, "">;
21
28
  type ColumnMapper = ReturnType<typeof ColumnMapper>;
@@ -17,8 +17,11 @@ export declare function detectFieldType(header: string): StandardFieldType | nul
17
17
  /**
18
18
  * Auto-detect column mapping from headers
19
19
  * Returns suggested mapping based on alias matching
20
+ * @param headers - CSV column headers
21
+ * @param fieldsConfig - Field configuration for the import mode
22
+ * @param mode - Import mode (used to customize detection behavior)
20
23
  */
21
- export declare function detectColumnMapping(headers: string[], fieldsConfig: ImportFieldsConfig): ColumnMapping;
24
+ export declare function detectColumnMapping(headers: string[], fieldsConfig: ImportFieldsConfig, mode?: ImportMode): ColumnMapping;
22
25
  /**
23
26
  * Validate that all required fields are mapped
24
27
  */
@@ -50,7 +53,7 @@ export declare function parseHeaderLine(line: string, delimiter: ',' | ';'): str
50
53
  /**
51
54
  * Get CSV headers and suggested mapping
52
55
  */
53
- export declare function analyzeCSVHeaders(csvContent: string, fieldsConfig: ImportFieldsConfig): {
56
+ export declare function analyzeCSVHeaders(csvContent: string, fieldsConfig: ImportFieldsConfig, mode?: ImportMode): {
54
57
  headers: string[];
55
58
  delimiter: ',' | ';';
56
59
  suggestedMapping: ColumnMapping;
@@ -96,14 +96,21 @@ export function detectFieldType(header) {
96
96
  /**
97
97
  * Auto-detect column mapping from headers
98
98
  * Returns suggested mapping based on alias matching
99
+ * @param headers - CSV column headers
100
+ * @param fieldsConfig - Field configuration for the import mode
101
+ * @param mode - Import mode (used to customize detection behavior)
99
102
  */
100
- export function detectColumnMapping(headers, fieldsConfig) {
103
+ export function detectColumnMapping(headers, fieldsConfig, mode) {
101
104
  const mapping = {};
102
105
  const usedIndices = new Set();
103
106
  // Get all field types we're looking for
104
107
  const allFields = [...fieldsConfig.required, ...fieldsConfig.optional];
108
+ // For point mode, skip auto-detecting ID - default to auto-generated
109
+ const fieldsToDetect = mode === 'point'
110
+ ? allFields.filter(f => f.type !== 'id')
111
+ : allFields;
105
112
  // First pass: exact matches and strong matches
106
- for (const field of allFields) {
113
+ for (const field of fieldsToDetect) {
107
114
  const aliases = COLUMN_ALIASES[field.type] || [];
108
115
  for (let i = 0; i < headers.length; i++) {
109
116
  if (usedIndices.has(i))
@@ -118,7 +125,7 @@ export function detectColumnMapping(headers, fieldsConfig) {
118
125
  }
119
126
  }
120
127
  // Second pass: partial matches for unmapped fields
121
- for (const field of allFields) {
128
+ for (const field of fieldsToDetect) {
122
129
  if (mapping[field.type] !== undefined)
123
130
  continue;
124
131
  const aliases = COLUMN_ALIASES[field.type] || [];
@@ -190,7 +197,7 @@ export function detectImportMode(headers) {
190
197
  export function autoDetectImport(headers) {
191
198
  const mode = detectImportMode(headers);
192
199
  const fieldsConfig = getFieldsForMode(mode);
193
- const mapping = detectColumnMapping(headers, fieldsConfig);
200
+ const mapping = detectColumnMapping(headers, fieldsConfig, mode);
194
201
  return { mode, mapping, fieldsConfig };
195
202
  }
196
203
  /**
@@ -211,14 +218,14 @@ export function parseHeaderLine(line, delimiter) {
211
218
  /**
212
219
  * Get CSV headers and suggested mapping
213
220
  */
214
- export function analyzeCSVHeaders(csvContent, fieldsConfig) {
221
+ export function analyzeCSVHeaders(csvContent, fieldsConfig, mode) {
215
222
  const lines = csvContent.trim().split('\n');
216
223
  if (lines.length === 0) {
217
224
  return { headers: [], delimiter: ',', suggestedMapping: {}, rowCount: 0 };
218
225
  }
219
226
  const delimiter = detectDelimiter(lines[0]);
220
227
  const headers = parseHeaderLine(lines[0], delimiter);
221
- const suggestedMapping = detectColumnMapping(headers, fieldsConfig);
228
+ const suggestedMapping = detectColumnMapping(headers, fieldsConfig, mode);
222
229
  return {
223
230
  headers,
224
231
  delimiter,
@@ -9,7 +9,7 @@
9
9
  /** Cell mode - ID resolves against existing cell data */
10
10
  export const CELL_MODE_FIELDS = {
11
11
  required: [
12
- { type: 'id', label: 'Cell ID', required: true, description: 'e.g. cellName, txId' }
12
+ { type: 'id', label: 'txId / (Cell Name) ', required: true, description: 'Column to match against cell data' }
13
13
  ],
14
14
  optional: [
15
15
  { type: 'group', label: 'Group', required: false, description: 'e.g. customGroup, category' },
@@ -19,11 +19,11 @@ export const CELL_MODE_FIELDS = {
19
19
  /** Point mode - creates markers at lat/lon coordinates */
20
20
  export const POINT_MODE_FIELDS = {
21
21
  required: [
22
- { type: 'id', label: 'ID / Name', required: true, description: 'e.g. name, label, id' },
23
22
  { type: 'lat', label: 'Latitude', required: true, description: 'e.g. lat, latitude, y' },
24
23
  { type: 'lon', label: 'Longitude', required: true, description: 'e.g. lon, lng, longitude, x' }
25
24
  ],
26
25
  optional: [
26
+ { type: 'id', label: 'Name / ID', required: false, description: 'Optional - auto-generated if not mapped' },
27
27
  { type: 'group', label: 'Group', required: false, description: 'e.g. category, type' },
28
28
  { type: 'sizeFactor', label: 'Size', required: false, description: 'e.g. size, weight' }
29
29
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.144",
3
+ "version": "0.0.147",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",