@smartnet360/svelte-components 0.0.142 → 0.0.144
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/dist/apps/antenna-tools/components/AntennaControls.svelte +14 -6
- package/dist/apps/antenna-tools/components/AntennaTools.svelte +6 -5
- package/dist/core/Auth/auth.svelte.js +72 -39
- package/dist/core/Auth/config.d.ts +1 -0
- package/dist/core/Auth/config.js +3 -4
- package/dist/map-v3/demo/DemoMap.svelte +3 -1
- package/dist/map-v3/features/custom/components/CustomCellSetManager.svelte +205 -14
- package/dist/map-v3/features/custom/components/CustomCellSetManager.svelte.d.ts +3 -0
- package/dist/map-v3/features/custom/components/ServerSetBrowser.svelte +398 -0
- package/dist/map-v3/features/custom/components/ServerSetBrowser.svelte.d.ts +22 -0
- package/dist/map-v3/features/custom/components/index.d.ts +1 -0
- package/dist/map-v3/features/custom/components/index.js +1 -0
- package/dist/map-v3/features/custom/db/custom-sets-api.d.ts +65 -0
- package/dist/map-v3/features/custom/db/custom-sets-api.js +220 -0
- package/dist/map-v3/features/custom/db/custom-sets-repository.d.ts +77 -0
- package/dist/map-v3/features/custom/db/custom-sets-repository.js +195 -0
- package/dist/map-v3/features/custom/db/index.d.ts +10 -0
- package/dist/map-v3/features/custom/db/index.js +9 -0
- package/dist/map-v3/features/custom/db/schema.sql +102 -0
- package/dist/map-v3/features/custom/db/types.d.ts +95 -0
- package/dist/map-v3/features/custom/db/types.js +95 -0
- package/dist/map-v3/features/custom/index.d.ts +2 -0
- package/dist/map-v3/features/custom/index.js +2 -0
- package/dist/map-v3/features/custom/logic/csv-parser.d.ts +12 -1
- package/dist/map-v3/features/custom/logic/csv-parser.js +54 -16
- package/dist/map-v3/features/custom/logic/tree-adapter.js +5 -3
- package/dist/map-v3/features/custom/stores/custom-cell-sets.svelte.d.ts +5 -1
- package/dist/map-v3/features/custom/stores/custom-cell-sets.svelte.js +6 -3
- package/dist/shared/csv-import/ColumnMapper.svelte +194 -0
- package/dist/shared/csv-import/ColumnMapper.svelte.d.ts +22 -0
- package/dist/shared/csv-import/column-detector.d.ts +58 -0
- package/dist/shared/csv-import/column-detector.js +228 -0
- package/dist/shared/csv-import/index.d.ts +10 -0
- package/dist/shared/csv-import/index.js +12 -0
- package/dist/shared/csv-import/types.d.ts +67 -0
- package/dist/shared/csv-import/types.js +70 -0
- package/package.json +1 -1
|
@@ -67,6 +67,10 @@
|
|
|
67
67
|
// Avoid overwriting user selections
|
|
68
68
|
if (selectedAntenna !== internalSelectedAntenna) {
|
|
69
69
|
internalSelectedAntenna = selectedAntenna;
|
|
70
|
+
// Also sync frequency when antenna is set externally
|
|
71
|
+
if (selectedAntenna && selectedAntenna.frequency) {
|
|
72
|
+
selectedFrequency = selectedAntenna.frequency;
|
|
73
|
+
}
|
|
70
74
|
}
|
|
71
75
|
});
|
|
72
76
|
|
|
@@ -76,12 +80,13 @@
|
|
|
76
80
|
let newTilts: string[] = ['0'];
|
|
77
81
|
|
|
78
82
|
if (internalSelectedAntenna) {
|
|
79
|
-
// Find
|
|
83
|
+
// Find antennas with the same name
|
|
80
84
|
let sameName = antennas.filter(a => a.name === internalSelectedAntenna!.name);
|
|
81
85
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
// Always filter by frequency - use selectedFrequency or fall back to antenna's frequency
|
|
87
|
+
const freqToUse = selectedFrequency ?? internalSelectedAntenna.frequency;
|
|
88
|
+
if (freqToUse) {
|
|
89
|
+
sameName = sameName.filter(a => a.frequency === freqToUse);
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
const allTilts = new Set<string>();
|
|
@@ -114,12 +119,15 @@
|
|
|
114
119
|
// Handle case where availableElectricalTilts might not be populated yet
|
|
115
120
|
const targetTilt = availableElectricalTilts[tiltIndex] || '0';
|
|
116
121
|
|
|
122
|
+
// Always use frequency filter - selectedFrequency or the current antenna's frequency
|
|
123
|
+
const freqToUse = selectedFrequency ?? internalSelectedAntenna?.frequency;
|
|
124
|
+
|
|
117
125
|
// Find antenna with matching name, frequency, and electrical tilt
|
|
118
126
|
return antennas.find(antenna => {
|
|
119
127
|
if (antenna.name !== antennaName) return false;
|
|
120
128
|
|
|
121
|
-
//
|
|
122
|
-
if (
|
|
129
|
+
// Must match frequency
|
|
130
|
+
if (freqToUse && antenna.frequency !== freqToUse) return false;
|
|
123
131
|
|
|
124
132
|
if (antenna.tilt) {
|
|
125
133
|
const tiltString = antenna.tilt.toString();
|
|
@@ -197,10 +197,10 @@
|
|
|
197
197
|
|
|
198
198
|
// === Helpers ===
|
|
199
199
|
function updateAvailableTilts(antenna: Antenna) {
|
|
200
|
-
// Collect
|
|
201
|
-
const
|
|
200
|
+
// Collect available tilts from antennas with the same name AND frequency
|
|
201
|
+
const sameNameAndFreq = antennas.filter(a => a.name === antenna.name && a.frequency === antenna.frequency);
|
|
202
202
|
const allTilts = new Set<string>();
|
|
203
|
-
|
|
203
|
+
sameNameAndFreq.forEach(a => {
|
|
204
204
|
if (a.tilt) {
|
|
205
205
|
const tiltStr = a.tilt.toString();
|
|
206
206
|
if (tiltStr.includes(',')) {
|
|
@@ -214,9 +214,10 @@
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
function getAvailableTiltsForAntenna(antenna: Antenna): string[] {
|
|
217
|
-
|
|
217
|
+
// Collect available tilts from antennas with the same name AND frequency
|
|
218
|
+
const sameNameAndFreq = antennas.filter(a => a.name === antenna.name && a.frequency === antenna.frequency);
|
|
218
219
|
const allTilts = new Set<string>();
|
|
219
|
-
|
|
220
|
+
sameNameAndFreq.forEach(a => {
|
|
220
221
|
if (a.tilt) {
|
|
221
222
|
const tiltStr = a.tilt.toString();
|
|
222
223
|
if (tiltStr.includes(',')) {
|
|
@@ -2,30 +2,61 @@
|
|
|
2
2
|
import { browser } from '$app/environment';
|
|
3
3
|
// Demo mode - enabled via PUBLIC_DEMO_MODE env var (GitHub Pages)
|
|
4
4
|
const DEMO_MODE = import.meta.env.PUBLIC_DEMO_MODE === 'true';
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
// Demo user
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
// Test users configuration (no backend required)
|
|
6
|
+
const TEST_USERS = {
|
|
7
|
+
// Demo user (view-only permissions)
|
|
8
|
+
demo: {
|
|
9
|
+
password: 'demo123',
|
|
10
|
+
session: {
|
|
11
|
+
user: {
|
|
12
|
+
id: 'demo-user',
|
|
13
|
+
username: 'demo',
|
|
14
|
+
displayName: 'Demo User',
|
|
15
|
+
email: 'demo@example.com',
|
|
16
|
+
groups: ['demo'],
|
|
17
|
+
loginTime: Date.now()
|
|
18
|
+
},
|
|
19
|
+
permissions: [
|
|
20
|
+
{ featureId: 'map-view', canView: true, canEdit: false, canAdmin: false },
|
|
21
|
+
{ featureId: 'coverage-view', canView: true, canEdit: false, canAdmin: false },
|
|
22
|
+
{ featureId: 'charts-view', canView: true, canEdit: false, canAdmin: false },
|
|
23
|
+
{ featureId: 'desktop-view', canView: true, canEdit: false, canAdmin: false },
|
|
24
|
+
{ featureId: 'table-view', canView: true, canEdit: false, canAdmin: false },
|
|
25
|
+
{ featureId: 'antenna-view', canView: true, canEdit: false, canAdmin: false },
|
|
26
|
+
{ featureId: 'site-check', canView: true, canEdit: false, canAdmin: false },
|
|
27
|
+
{ featureId: 'settings-view', canView: true, canEdit: false, canAdmin: false }
|
|
28
|
+
],
|
|
29
|
+
token: 'demo-token',
|
|
30
|
+
expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours
|
|
31
|
+
}
|
|
16
32
|
},
|
|
17
|
-
permissions
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
// Test user (full permissions, for testing without backend)
|
|
34
|
+
test: {
|
|
35
|
+
password: 'test',
|
|
36
|
+
session: {
|
|
37
|
+
user: {
|
|
38
|
+
id: 'test-user',
|
|
39
|
+
username: 'test',
|
|
40
|
+
displayName: 'Test User',
|
|
41
|
+
email: 'test@localhost',
|
|
42
|
+
groups: ['testers', 'admins'],
|
|
43
|
+
loginTime: Date.now()
|
|
44
|
+
},
|
|
45
|
+
permissions: [
|
|
46
|
+
{ featureId: 'map-view', canView: true, canEdit: true, canAdmin: true },
|
|
47
|
+
{ featureId: 'coverage-view', canView: true, canEdit: true, canAdmin: true },
|
|
48
|
+
{ featureId: 'charts-view', canView: true, canEdit: true, canAdmin: true },
|
|
49
|
+
{ featureId: 'desktop-view', canView: true, canEdit: true, canAdmin: true },
|
|
50
|
+
{ featureId: 'table-view', canView: true, canEdit: true, canAdmin: true },
|
|
51
|
+
{ featureId: 'antenna-view', canView: true, canEdit: true, canAdmin: true },
|
|
52
|
+
{ featureId: 'site-check', canView: true, canEdit: true, canAdmin: true },
|
|
53
|
+
{ featureId: 'settings-view', canView: true, canEdit: true, canAdmin: true },
|
|
54
|
+
{ featureId: 'admin', canView: true, canEdit: true, canAdmin: true }
|
|
55
|
+
],
|
|
56
|
+
token: 'test-token',
|
|
57
|
+
expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours
|
|
58
|
+
}
|
|
59
|
+
}
|
|
29
60
|
};
|
|
30
61
|
// Default configuration
|
|
31
62
|
const defaultConfig = {
|
|
@@ -107,22 +138,22 @@ export function createAuthState(config = {}) {
|
|
|
107
138
|
isLoading = true;
|
|
108
139
|
error = null;
|
|
109
140
|
try {
|
|
110
|
-
//
|
|
141
|
+
// Check for test users (works in demo mode or as fallback)
|
|
142
|
+
const testUser = TEST_USERS[username.toLowerCase()];
|
|
143
|
+
if (testUser && password === testUser.password) {
|
|
144
|
+
// Create fresh session with updated timestamp
|
|
145
|
+
const testSession = {
|
|
146
|
+
...testUser.session,
|
|
147
|
+
user: { ...testUser.session.user, loginTime: Date.now() },
|
|
148
|
+
expiresAt: Date.now() + (24 * 60 * 60 * 1000)
|
|
149
|
+
};
|
|
150
|
+
saveSession(testSession);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
// In demo mode, only allow test users
|
|
111
154
|
if (DEMO_MODE) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const demoSession = {
|
|
115
|
-
...DEMO_SESSION,
|
|
116
|
-
user: { ...DEMO_SESSION.user, loginTime: Date.now() },
|
|
117
|
-
expiresAt: Date.now() + (24 * 60 * 60 * 1000)
|
|
118
|
-
};
|
|
119
|
-
saveSession(demoSession);
|
|
120
|
-
return true;
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
error = 'Demo mode: Use username "demo" and password "demo123"';
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
155
|
+
error = 'Demo mode: Use "demo/demo123" or "test/test" to login';
|
|
156
|
+
return false;
|
|
126
157
|
}
|
|
127
158
|
// Normal mode: call server API
|
|
128
159
|
const response = await fetch(`${cfg.apiEndpoint}/login`, {
|
|
@@ -141,7 +172,9 @@ export function createAuthState(config = {}) {
|
|
|
141
172
|
}
|
|
142
173
|
}
|
|
143
174
|
catch (e) {
|
|
144
|
-
|
|
175
|
+
// If server is unavailable, show helpful message about test users
|
|
176
|
+
const networkError = e instanceof Error ? e.message : 'Network error';
|
|
177
|
+
error = `${networkError}. Try "test/test" for testing without a backend.`;
|
|
145
178
|
return false;
|
|
146
179
|
}
|
|
147
180
|
finally {
|
|
@@ -21,5 +21,6 @@ export declare const DEFAULT_DEV_PERMISSIONS: FeatureAccess[];
|
|
|
21
21
|
export declare function getDevConfig(): DevConfig;
|
|
22
22
|
/**
|
|
23
23
|
* Check if we're in development mode
|
|
24
|
+
* Currently disabled - authentication is always required
|
|
24
25
|
*/
|
|
25
26
|
export declare function isDevMode(): boolean;
|
package/dist/core/Auth/config.js
CHANGED
|
@@ -246,11 +246,10 @@ export function getDevConfig() {
|
|
|
246
246
|
}
|
|
247
247
|
/**
|
|
248
248
|
* Check if we're in development mode
|
|
249
|
+
* Currently disabled - authentication is always required
|
|
249
250
|
*/
|
|
250
251
|
export function isDevMode() {
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
return true;
|
|
254
|
-
}
|
|
252
|
+
// Auto-login disabled - always require authentication
|
|
253
|
+
// Use test/test or demo/demo123 for testing without a backend
|
|
255
254
|
return false;
|
|
256
255
|
}
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
import {
|
|
25
25
|
CustomCellsLayer,
|
|
26
26
|
CustomCellSetManager,
|
|
27
|
-
createCustomCellSetsStore
|
|
27
|
+
createCustomCellSetsStore,
|
|
28
|
+
createCustomSetsApi
|
|
28
29
|
} from '../features/custom';
|
|
29
30
|
// Custom Sites Feature
|
|
30
31
|
import {
|
|
@@ -103,6 +104,7 @@
|
|
|
103
104
|
<CustomCellSetManager
|
|
104
105
|
position="top-left"
|
|
105
106
|
setsStore={customCellSets}
|
|
107
|
+
apiClient={createCustomSetsApi('/api/custom-sets')}
|
|
106
108
|
/>
|
|
107
109
|
|
|
108
110
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - Quick Add (paste cell names)
|
|
8
8
|
* - Global size/opacity sliders
|
|
9
9
|
* - Collapsible set sections with TreeView + color pickers
|
|
10
|
+
* - Server sharing (optional, when apiClient provided)
|
|
10
11
|
*/
|
|
11
12
|
import { untrack } from 'svelte';
|
|
12
13
|
import { MapControl } from '../../../shared';
|
|
@@ -14,17 +15,30 @@
|
|
|
14
15
|
import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
|
|
15
16
|
import type { CustomCellImportResult, CustomCellSet } from '../types';
|
|
16
17
|
import { buildCustomCellTree } from '../logic/tree-adapter';
|
|
18
|
+
import type { CustomSetsApiClient } from '../db/custom-sets-api';
|
|
19
|
+
import ServerSetBrowser from './ServerSetBrowser.svelte';
|
|
20
|
+
import { extractCsvHeaders } from '../logic/csv-parser';
|
|
21
|
+
import {
|
|
22
|
+
ColumnMapper,
|
|
23
|
+
autoDetectImport,
|
|
24
|
+
getFieldsForMode,
|
|
25
|
+
type ColumnMapping,
|
|
26
|
+
type ImportMode
|
|
27
|
+
} from '../../../../shared/csv-import';
|
|
17
28
|
|
|
18
29
|
interface Props {
|
|
19
30
|
/** The custom cell sets store */
|
|
20
31
|
setsStore: CustomCellSetsStore;
|
|
21
32
|
/** Control position on map */
|
|
22
33
|
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
34
|
+
/** Optional API client for server sharing */
|
|
35
|
+
apiClient?: CustomSetsApiClient;
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
let {
|
|
26
39
|
setsStore,
|
|
27
|
-
position = 'top-left'
|
|
40
|
+
position = 'top-left',
|
|
41
|
+
apiClient
|
|
28
42
|
}: Props = $props();
|
|
29
43
|
|
|
30
44
|
// Global display settings - initialized from first set if available
|
|
@@ -51,12 +65,24 @@
|
|
|
51
65
|
let importError = $state('');
|
|
52
66
|
let fileInput: HTMLInputElement;
|
|
53
67
|
|
|
68
|
+
// Column mapping state
|
|
69
|
+
let showMappingModal = $state(false);
|
|
70
|
+
let pendingCsvContent = $state<string | null>(null);
|
|
71
|
+
let csvHeaders = $state<string[]>([]);
|
|
72
|
+
let columnMapping = $state<ColumnMapping>({});
|
|
73
|
+
let importMode = $state<ImportMode>('cell');
|
|
74
|
+
|
|
54
75
|
// Quick Add state
|
|
55
76
|
let showQuickAdd = $state(false);
|
|
56
77
|
let quickAddText = $state('');
|
|
57
78
|
let quickAddName = $state('Quick Selection');
|
|
58
79
|
let quickAddResult = $state<CustomCellImportResult | null>(null);
|
|
59
80
|
|
|
81
|
+
// Server sharing state
|
|
82
|
+
let showServerBrowser = $state(false);
|
|
83
|
+
let savingSetId = $state<string | null>(null);
|
|
84
|
+
let saveError = $state('');
|
|
85
|
+
|
|
60
86
|
// Track expanded sets
|
|
61
87
|
let expandedSets = $state<Set<string>>(new Set());
|
|
62
88
|
|
|
@@ -143,8 +169,23 @@
|
|
|
143
169
|
reader.onload = (e) => {
|
|
144
170
|
try {
|
|
145
171
|
const content = e.target?.result as string;
|
|
146
|
-
|
|
147
|
-
|
|
172
|
+
// Extract headers and show mapping modal
|
|
173
|
+
const headers = extractCsvHeaders(content);
|
|
174
|
+
if (headers.length === 0) {
|
|
175
|
+
throw new Error('CSV file appears to be empty');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Store content for later parsing
|
|
179
|
+
pendingCsvContent = content;
|
|
180
|
+
csvHeaders = headers;
|
|
181
|
+
|
|
182
|
+
// Auto-detect import mode and column mapping
|
|
183
|
+
const detected = autoDetectImport(headers);
|
|
184
|
+
importMode = detected.mode;
|
|
185
|
+
columnMapping = detected.mapping;
|
|
186
|
+
|
|
187
|
+
// Show mapping modal
|
|
188
|
+
showMappingModal = true;
|
|
148
189
|
} catch (err) {
|
|
149
190
|
importError = err instanceof Error ? err.message : 'Failed to parse CSV';
|
|
150
191
|
importResult = null;
|
|
@@ -157,6 +198,28 @@
|
|
|
157
198
|
reader.readAsText(file);
|
|
158
199
|
input.value = '';
|
|
159
200
|
}
|
|
201
|
+
|
|
202
|
+
function confirmMapping() {
|
|
203
|
+
if (!pendingCsvContent) return;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
importResult = setsStore.importFromCsv(pendingCsvContent, importFileName, columnMapping);
|
|
207
|
+
showMappingModal = false;
|
|
208
|
+
showImportModal = true;
|
|
209
|
+
} catch (err) {
|
|
210
|
+
importError = err instanceof Error ? err.message : 'Failed to parse CSV';
|
|
211
|
+
showMappingModal = false;
|
|
212
|
+
importResult = null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function closeMappingModal() {
|
|
217
|
+
showMappingModal = false;
|
|
218
|
+
pendingCsvContent = null;
|
|
219
|
+
csvHeaders = [];
|
|
220
|
+
columnMapping = {};
|
|
221
|
+
importMode = 'cell';
|
|
222
|
+
}
|
|
160
223
|
|
|
161
224
|
function confirmImport() {
|
|
162
225
|
if (!importResult) return;
|
|
@@ -223,6 +286,26 @@
|
|
|
223
286
|
}
|
|
224
287
|
}
|
|
225
288
|
|
|
289
|
+
// Server sharing functions
|
|
290
|
+
async function saveSetToServer(set: CustomCellSet) {
|
|
291
|
+
if (!apiClient) return;
|
|
292
|
+
|
|
293
|
+
savingSetId = set.id;
|
|
294
|
+
saveError = '';
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const result = await apiClient.saveSet(set);
|
|
298
|
+
|
|
299
|
+
if (!result.success) {
|
|
300
|
+
saveError = result.error || 'Failed to save to server';
|
|
301
|
+
}
|
|
302
|
+
} catch (err) {
|
|
303
|
+
saveError = err instanceof Error ? err.message : 'Failed to save';
|
|
304
|
+
} finally {
|
|
305
|
+
savingSetId = null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
226
309
|
function toggleExpanded(setId: string) {
|
|
227
310
|
if (expandedSets.has(setId)) {
|
|
228
311
|
expandedSets.delete(setId);
|
|
@@ -252,9 +335,20 @@
|
|
|
252
335
|
|
|
253
336
|
<MapControl {position} title="Custom Layers" icon="layers" controlWidth="320px">
|
|
254
337
|
{#snippet actions()}
|
|
338
|
+
{#if apiClient}
|
|
339
|
+
<button
|
|
340
|
+
class="btn btn-sm btn-outline-primary border-0 p-1 px-2"
|
|
341
|
+
title="Load from Server"
|
|
342
|
+
aria-label="Load from Server"
|
|
343
|
+
onclick={() => showServerBrowser = true}
|
|
344
|
+
>
|
|
345
|
+
<i class="bi bi-cloud-download"></i>
|
|
346
|
+
</button>
|
|
347
|
+
{/if}
|
|
255
348
|
<button
|
|
256
349
|
class="btn btn-sm btn-outline-primary border-0 p-1 px-2"
|
|
257
350
|
title="Quick Add (paste cell names)"
|
|
351
|
+
aria-label="Quick Add"
|
|
258
352
|
onclick={() => showQuickAdd = true}
|
|
259
353
|
>
|
|
260
354
|
<i class="bi bi-pencil-square"></i>
|
|
@@ -262,6 +356,7 @@
|
|
|
262
356
|
<button
|
|
263
357
|
class="btn btn-sm btn-outline-primary border-0 p-1 px-2"
|
|
264
358
|
title="Import CSV"
|
|
359
|
+
aria-label="Import CSV"
|
|
265
360
|
onclick={triggerFileInput}
|
|
266
361
|
>
|
|
267
362
|
<i class="bi bi-upload"></i>
|
|
@@ -290,6 +385,22 @@
|
|
|
290
385
|
</div>
|
|
291
386
|
{/if}
|
|
292
387
|
|
|
388
|
+
<!-- Server save error -->
|
|
389
|
+
{#if saveError}
|
|
390
|
+
<div class="alert alert-warning py-1 px-2 mb-2 small d-flex justify-content-between align-items-center">
|
|
391
|
+
<span>
|
|
392
|
+
<i class="bi bi-cloud-slash me-1"></i>
|
|
393
|
+
{saveError}
|
|
394
|
+
</span>
|
|
395
|
+
<button
|
|
396
|
+
type="button"
|
|
397
|
+
class="btn-close btn-sm"
|
|
398
|
+
aria-label="Dismiss"
|
|
399
|
+
onclick={() => saveError = ''}
|
|
400
|
+
></button>
|
|
401
|
+
</div>
|
|
402
|
+
{/if}
|
|
403
|
+
|
|
293
404
|
<!-- Empty state -->
|
|
294
405
|
{#if setsStore.sets.length === 0}
|
|
295
406
|
<div class="text-center p-3">
|
|
@@ -297,7 +408,15 @@
|
|
|
297
408
|
<p class="text-muted small mt-2 mb-0">
|
|
298
409
|
No custom layers loaded.
|
|
299
410
|
</p>
|
|
300
|
-
<div class="d-flex gap-2 justify-content-center mt-2">
|
|
411
|
+
<!-- <div class="d-flex gap-2 justify-content-center mt-2 flex-wrap">
|
|
412
|
+
{#if apiClient}
|
|
413
|
+
<button
|
|
414
|
+
class="btn btn-sm btn-outline-success"
|
|
415
|
+
onclick={() => showServerBrowser = true}
|
|
416
|
+
>
|
|
417
|
+
<i class="bi bi-cloud-download me-1"></i> Load from Server
|
|
418
|
+
</button>
|
|
419
|
+
{/if}
|
|
301
420
|
<button
|
|
302
421
|
class="btn btn-sm btn-outline-primary"
|
|
303
422
|
onclick={() => showQuickAdd = true}
|
|
@@ -310,7 +429,7 @@
|
|
|
310
429
|
>
|
|
311
430
|
<i class="bi bi-upload me-1"></i> Import CSV
|
|
312
431
|
</button>
|
|
313
|
-
</div>
|
|
432
|
+
</div> -->
|
|
314
433
|
</div>
|
|
315
434
|
{:else}
|
|
316
435
|
<!-- Global Controls -->
|
|
@@ -323,8 +442,8 @@
|
|
|
323
442
|
<input
|
|
324
443
|
type="range"
|
|
325
444
|
class="form-range"
|
|
326
|
-
min="
|
|
327
|
-
max="
|
|
445
|
+
min="5"
|
|
446
|
+
max="100"
|
|
328
447
|
step="5"
|
|
329
448
|
value={globalSectorSize}
|
|
330
449
|
oninput={handleGlobalSectorSizeChange}
|
|
@@ -390,13 +509,31 @@
|
|
|
390
509
|
<small class="text-muted">{getSetItemCounts(set)} · {set.groups.length} groups</small>
|
|
391
510
|
</div>
|
|
392
511
|
</div>
|
|
393
|
-
<
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
512
|
+
<div class="d-flex gap-1">
|
|
513
|
+
{#if apiClient}
|
|
514
|
+
<button
|
|
515
|
+
class="btn btn-sm btn-outline-primary border-0 p-1"
|
|
516
|
+
title="Share to Server"
|
|
517
|
+
aria-label="Share to Server"
|
|
518
|
+
onclick={() => saveSetToServer(set)}
|
|
519
|
+
disabled={savingSetId === set.id}
|
|
520
|
+
>
|
|
521
|
+
{#if savingSetId === set.id}
|
|
522
|
+
<span class="spinner-border spinner-border-sm" role="status"></span>
|
|
523
|
+
{:else}
|
|
524
|
+
<i class="bi bi-cloud-upload"></i>
|
|
525
|
+
{/if}
|
|
526
|
+
</button>
|
|
527
|
+
{/if}
|
|
528
|
+
<button
|
|
529
|
+
class="btn btn-sm btn-outline-danger border-0 p-1"
|
|
530
|
+
title="Remove"
|
|
531
|
+
aria-label="Remove set"
|
|
532
|
+
onclick={() => removeSet(set.id)}
|
|
533
|
+
>
|
|
534
|
+
<i class="bi bi-trash"></i>
|
|
535
|
+
</button>
|
|
536
|
+
</div>
|
|
400
537
|
</div>
|
|
401
538
|
|
|
402
539
|
<!-- Set Content (TreeView) -->
|
|
@@ -438,6 +575,50 @@
|
|
|
438
575
|
</div>
|
|
439
576
|
</MapControl>
|
|
440
577
|
|
|
578
|
+
<!-- Column Mapping Modal -->
|
|
579
|
+
{#if showMappingModal && csvHeaders.length > 0}
|
|
580
|
+
{@const fieldsConfig = getFieldsForMode(importMode)}
|
|
581
|
+
{@const isValid = fieldsConfig.required.every(f =>
|
|
582
|
+
columnMapping[f.type] !== null && columnMapping[f.type] !== undefined
|
|
583
|
+
)}
|
|
584
|
+
<div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
|
|
585
|
+
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
|
|
586
|
+
<div class="modal-content">
|
|
587
|
+
<div class="modal-header py-2">
|
|
588
|
+
<h6 class="modal-title">
|
|
589
|
+
<i class="bi bi-grid-3x3 me-2"></i>
|
|
590
|
+
Import: {importFileName}
|
|
591
|
+
</h6>
|
|
592
|
+
<button type="button" class="btn-close" aria-label="Close" onclick={closeMappingModal}></button>
|
|
593
|
+
</div>
|
|
594
|
+
<div class="modal-body py-2">
|
|
595
|
+
<ColumnMapper
|
|
596
|
+
headers={csvHeaders}
|
|
597
|
+
mode={importMode}
|
|
598
|
+
mapping={columnMapping}
|
|
599
|
+
onmodechange={(m) => importMode = m}
|
|
600
|
+
onchange={(m) => columnMapping = m}
|
|
601
|
+
/>
|
|
602
|
+
</div>
|
|
603
|
+
<div class="modal-footer py-2">
|
|
604
|
+
<button type="button" class="btn btn-secondary btn-sm" onclick={closeMappingModal}>
|
|
605
|
+
Cancel
|
|
606
|
+
</button>
|
|
607
|
+
<button
|
|
608
|
+
type="button"
|
|
609
|
+
class="btn btn-primary btn-sm"
|
|
610
|
+
onclick={confirmMapping}
|
|
611
|
+
disabled={!isValid}
|
|
612
|
+
>
|
|
613
|
+
<i class="bi bi-arrow-right me-1"></i>
|
|
614
|
+
Continue
|
|
615
|
+
</button>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
{/if}
|
|
621
|
+
|
|
441
622
|
<!-- Import Result Modal -->
|
|
442
623
|
{#if showImportModal && importResult}
|
|
443
624
|
<div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
|
|
@@ -635,6 +816,16 @@
|
|
|
635
816
|
</div>
|
|
636
817
|
{/if}
|
|
637
818
|
|
|
819
|
+
<!-- Server Set Browser Modal -->
|
|
820
|
+
{#if apiClient}
|
|
821
|
+
<ServerSetBrowser
|
|
822
|
+
{apiClient}
|
|
823
|
+
{setsStore}
|
|
824
|
+
show={showServerBrowser}
|
|
825
|
+
onclose={() => showServerBrowser = false}
|
|
826
|
+
/>
|
|
827
|
+
{/if}
|
|
828
|
+
|
|
638
829
|
<style>
|
|
639
830
|
.custom-layers-manager {
|
|
640
831
|
font-size: 0.875rem;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
|
|
2
|
+
import type { CustomSetsApiClient } from '../db/custom-sets-api';
|
|
2
3
|
interface Props {
|
|
3
4
|
/** The custom cell sets store */
|
|
4
5
|
setsStore: CustomCellSetsStore;
|
|
5
6
|
/** Control position on map */
|
|
6
7
|
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
8
|
+
/** Optional API client for server sharing */
|
|
9
|
+
apiClient?: CustomSetsApiClient;
|
|
7
10
|
}
|
|
8
11
|
declare const CustomCellSetManager: import("svelte").Component<Props, {}, "">;
|
|
9
12
|
type CustomCellSetManager = ReturnType<typeof CustomCellSetManager>;
|