mcp-config-manager 1.0.13 → 2.1.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/README.md +81 -5
- package/package.json +1 -1
- package/public/index.html +104 -19
- package/public/js/clientView.js +53 -19
- package/public/js/iconPreferences.js +98 -0
- package/public/js/kanbanView.js +25 -42
- package/public/js/logoService.js +392 -0
- package/public/js/main.js +21 -1
- package/public/js/modals.js +344 -24
- package/public/js/popularMcps.js +304 -0
- package/public/js/serverView.js +24 -7
- package/public/mcp-logos.json +26 -0
- package/public/popular-mcps.json +555 -0
- package/public/style.css +617 -1
- package/src/config-manager.js +3 -5
package/public/js/modals.js
CHANGED
|
@@ -3,17 +3,284 @@ import {
|
|
|
3
3
|
addServerApi, updateServerApi, deleteServerApi, copyServerApi,
|
|
4
4
|
exportConfigApi, exportServerApi, importConfigApi, getClientConfigApi, renameServerApi, updateServerEnvApi, updateServerInClientsApi
|
|
5
5
|
} from './api.js';
|
|
6
|
+
import { getIconPreference, setIconPreference, removeIconPreference } from './iconPreferences.js';
|
|
7
|
+
import { getServerLogo, getLogoByDomain, clearLogoCache, getInitials } from './logoService.js';
|
|
6
8
|
|
|
7
9
|
let clients = []; // This will be passed from main.js
|
|
8
10
|
let currentClient = null; // This will be passed from main.js
|
|
9
11
|
let loadClientsCallback = null; // Callback to main.js to reload all clients
|
|
10
12
|
|
|
13
|
+
// Icon editor state
|
|
14
|
+
let currentIconType = 'auto';
|
|
15
|
+
let currentIconValue = null;
|
|
16
|
+
let selectedSearchDomain = null;
|
|
17
|
+
|
|
11
18
|
export function initModals(clientData, currentClientData, loadClientsFn) {
|
|
12
19
|
clients = clientData;
|
|
13
20
|
currentClient = currentClientData;
|
|
14
21
|
loadClientsCallback = loadClientsFn;
|
|
15
22
|
}
|
|
16
23
|
|
|
24
|
+
// Popular services for icon search suggestions
|
|
25
|
+
const POPULAR_SERVICES = [
|
|
26
|
+
{ domain: 'github.com', name: 'GitHub' },
|
|
27
|
+
{ domain: 'gitlab.com', name: 'GitLab' },
|
|
28
|
+
{ domain: 'slack.com', name: 'Slack' },
|
|
29
|
+
{ domain: 'notion.so', name: 'Notion' },
|
|
30
|
+
{ domain: 'linear.app', name: 'Linear' },
|
|
31
|
+
{ domain: 'figma.com', name: 'Figma' },
|
|
32
|
+
{ domain: 'stripe.com', name: 'Stripe' },
|
|
33
|
+
{ domain: 'openai.com', name: 'OpenAI' },
|
|
34
|
+
{ domain: 'anthropic.com', name: 'Anthropic' },
|
|
35
|
+
{ domain: 'vercel.com', name: 'Vercel' },
|
|
36
|
+
{ domain: 'supabase.com', name: 'Supabase' },
|
|
37
|
+
{ domain: 'postgresql.org', name: 'PostgreSQL' },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize icon editor with current server's icon preference
|
|
42
|
+
* Now uses simplified inline UI with just icon preview + pencil edit button
|
|
43
|
+
*/
|
|
44
|
+
async function initIconEditor(serverName, serverConfig) {
|
|
45
|
+
// Reset state
|
|
46
|
+
currentIconType = 'auto';
|
|
47
|
+
currentIconValue = null;
|
|
48
|
+
selectedSearchDomain = null;
|
|
49
|
+
|
|
50
|
+
// Load current preference
|
|
51
|
+
if (serverName) {
|
|
52
|
+
const pref = getIconPreference(serverName);
|
|
53
|
+
if (pref) {
|
|
54
|
+
currentIconType = pref.type;
|
|
55
|
+
currentIconValue = pref.value;
|
|
56
|
+
if (pref.type === 'logodev') {
|
|
57
|
+
selectedSearchDomain = pref.value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Update mini preview
|
|
63
|
+
await updateIconPreviewMini(serverName, serverConfig);
|
|
64
|
+
|
|
65
|
+
// Setup click handler for the edit trigger
|
|
66
|
+
const iconEditTrigger = document.getElementById('iconEditTrigger');
|
|
67
|
+
if (iconEditTrigger) {
|
|
68
|
+
iconEditTrigger.onclick = () => {
|
|
69
|
+
showIconSearchModal(serverName, serverConfig);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Update the mini icon preview in the server name row
|
|
76
|
+
*/
|
|
77
|
+
async function updateIconPreviewMini(serverName, serverConfig) {
|
|
78
|
+
const previewContainer = document.getElementById('iconPreviewMini');
|
|
79
|
+
if (!previewContainer) return;
|
|
80
|
+
|
|
81
|
+
const initials = getInitials(serverName || 'New');
|
|
82
|
+
let logoUrl = null;
|
|
83
|
+
|
|
84
|
+
if (currentIconType === 'none') {
|
|
85
|
+
// Show initials only
|
|
86
|
+
previewContainer.innerHTML = `<div class="server-logo-fallback">${initials}</div>`;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (currentIconType === 'url' && currentIconValue) {
|
|
91
|
+
logoUrl = currentIconValue;
|
|
92
|
+
} else if (currentIconType === 'logodev' && currentIconValue) {
|
|
93
|
+
logoUrl = getLogoByDomain(currentIconValue);
|
|
94
|
+
} else if (currentIconType === 'auto') {
|
|
95
|
+
// Use auto-detection (skip custom prefs since we're in edit mode)
|
|
96
|
+
if (serverName) {
|
|
97
|
+
logoUrl = await getServerLogo(serverName, serverConfig, true);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (logoUrl) {
|
|
102
|
+
previewContainer.innerHTML = `
|
|
103
|
+
<img src="${logoUrl}" alt="${serverName || 'icon'}"
|
|
104
|
+
onerror="this.parentElement.innerHTML='<div class=\\'server-logo-fallback\\'>${initials}</div>'">
|
|
105
|
+
`;
|
|
106
|
+
} else {
|
|
107
|
+
previewContainer.innerHTML = `<div class="server-logo-fallback">${initials}</div>`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Show icon search modal with options for auto, search, URL, and none
|
|
113
|
+
*/
|
|
114
|
+
function showIconSearchModal(serverName, serverConfig) {
|
|
115
|
+
const modal = document.getElementById('iconSearchModal');
|
|
116
|
+
const searchInput = document.getElementById('iconSearchInput');
|
|
117
|
+
const resultsDiv = document.getElementById('iconSearchResults');
|
|
118
|
+
const suggestionsGrid = document.getElementById('suggestionsGrid');
|
|
119
|
+
const confirmBtn = document.getElementById('confirmIconSearch');
|
|
120
|
+
const cancelBtn = document.getElementById('cancelIconSearch');
|
|
121
|
+
|
|
122
|
+
// Track temporary selection state
|
|
123
|
+
let tempType = currentIconType;
|
|
124
|
+
let tempValue = currentIconValue;
|
|
125
|
+
let tempSelectedDomain = selectedSearchDomain;
|
|
126
|
+
|
|
127
|
+
// Reset search
|
|
128
|
+
searchInput.value = '';
|
|
129
|
+
|
|
130
|
+
// Build initial results with quick action buttons
|
|
131
|
+
function renderQuickActions() {
|
|
132
|
+
const autoSelected = tempType === 'auto';
|
|
133
|
+
const noneSelected = tempType === 'none';
|
|
134
|
+
|
|
135
|
+
let html = `
|
|
136
|
+
<div class="icon-quick-actions">
|
|
137
|
+
<button type="button" class="icon-quick-btn ${autoSelected ? 'selected' : ''}" data-action="auto">
|
|
138
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
139
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
140
|
+
<path d="M12 6v6l4 2"></path>
|
|
141
|
+
</svg>
|
|
142
|
+
Auto detect
|
|
143
|
+
</button>
|
|
144
|
+
<button type="button" class="icon-quick-btn ${noneSelected ? 'selected' : ''}" data-action="none">
|
|
145
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
146
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
147
|
+
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line>
|
|
148
|
+
</svg>
|
|
149
|
+
No icon
|
|
150
|
+
</button>
|
|
151
|
+
</div>
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
// Show currently selected logo if any
|
|
155
|
+
if (tempType === 'logodev' && tempSelectedDomain) {
|
|
156
|
+
html += renderSearchResultHtml(tempSelectedDomain, true);
|
|
157
|
+
} else if (tempType === 'url' && tempValue) {
|
|
158
|
+
html += `
|
|
159
|
+
<div class="icon-search-result selected">
|
|
160
|
+
<img src="${tempValue}" alt="custom" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 40 40%22><rect fill=%22%23e2e8f0%22 width=%2240%22 height=%2240%22/><text x=%2250%25%22 y=%2250%25%22 dominant-baseline=%22middle%22 text-anchor=%22middle%22 fill=%22%2364748b%22 font-size=%2214%22>?</text></svg>'">
|
|
161
|
+
<div class="icon-search-result-info">
|
|
162
|
+
<div class="icon-search-result-domain">Custom URL</div>
|
|
163
|
+
<div class="icon-search-result-url">${tempValue}</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
`;
|
|
167
|
+
} else if (!autoSelected && !noneSelected) {
|
|
168
|
+
html += '<div class="icon-search-hint">Search for a logo or enter a custom URL</div>';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return html;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderSearchResultHtml(domain, selected = false) {
|
|
175
|
+
const logoUrl = getLogoByDomain(domain, { size: 64 });
|
|
176
|
+
return `
|
|
177
|
+
<div class="icon-search-result ${selected ? 'selected' : ''}">
|
|
178
|
+
<img src="${logoUrl}" alt="${domain}" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 40 40%22><rect fill=%22%23e2e8f0%22 width=%2240%22 height=%2240%22/><text x=%2250%25%22 y=%2250%25%22 dominant-baseline=%22middle%22 text-anchor=%22middle%22 fill=%22%2364748b%22 font-size=%2214%22>?</text></svg>'">
|
|
179
|
+
<div class="icon-search-result-info">
|
|
180
|
+
<div class="icon-search-result-domain">${domain}</div>
|
|
181
|
+
<div class="icon-search-result-url">Logo from logo.dev</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
resultsDiv.innerHTML = renderQuickActions();
|
|
188
|
+
confirmBtn.disabled = false; // Always allow confirm since auto is valid
|
|
189
|
+
|
|
190
|
+
// Quick action button handlers
|
|
191
|
+
resultsDiv.addEventListener('click', (e) => {
|
|
192
|
+
const btn = e.target.closest('.icon-quick-btn');
|
|
193
|
+
if (btn) {
|
|
194
|
+
const action = btn.dataset.action;
|
|
195
|
+
if (action === 'auto') {
|
|
196
|
+
tempType = 'auto';
|
|
197
|
+
tempValue = null;
|
|
198
|
+
tempSelectedDomain = null;
|
|
199
|
+
} else if (action === 'none') {
|
|
200
|
+
tempType = 'none';
|
|
201
|
+
tempValue = null;
|
|
202
|
+
tempSelectedDomain = null;
|
|
203
|
+
}
|
|
204
|
+
resultsDiv.innerHTML = renderQuickActions();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Populate suggestions
|
|
209
|
+
suggestionsGrid.innerHTML = '';
|
|
210
|
+
POPULAR_SERVICES.forEach(service => {
|
|
211
|
+
const chip = document.createElement('div');
|
|
212
|
+
chip.className = 'suggestion-chip';
|
|
213
|
+
chip.innerHTML = `
|
|
214
|
+
<img src="${getLogoByDomain(service.domain, { size: 32 })}" alt="${service.name}" onerror="this.style.display='none'">
|
|
215
|
+
<span>${service.name}</span>
|
|
216
|
+
`;
|
|
217
|
+
chip.onclick = () => {
|
|
218
|
+
tempType = 'logodev';
|
|
219
|
+
tempSelectedDomain = service.domain;
|
|
220
|
+
tempValue = service.domain;
|
|
221
|
+
resultsDiv.innerHTML = renderQuickActions();
|
|
222
|
+
};
|
|
223
|
+
suggestionsGrid.appendChild(chip);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Search input handler - supports domain search and custom URL
|
|
227
|
+
searchInput.onkeydown = (e) => {
|
|
228
|
+
if (e.key === 'Enter') {
|
|
229
|
+
e.preventDefault();
|
|
230
|
+
const query = searchInput.value.trim();
|
|
231
|
+
if (query) {
|
|
232
|
+
// Check if it's a full URL
|
|
233
|
+
if (query.startsWith('http://') || query.startsWith('https://')) {
|
|
234
|
+
tempType = 'url';
|
|
235
|
+
tempValue = query;
|
|
236
|
+
tempSelectedDomain = null;
|
|
237
|
+
} else {
|
|
238
|
+
// Treat as domain
|
|
239
|
+
let domain = query.toLowerCase();
|
|
240
|
+
if (!domain.includes('.')) {
|
|
241
|
+
domain = `${domain}.com`;
|
|
242
|
+
}
|
|
243
|
+
tempType = 'logodev';
|
|
244
|
+
tempSelectedDomain = domain;
|
|
245
|
+
tempValue = domain;
|
|
246
|
+
}
|
|
247
|
+
resultsDiv.innerHTML = renderQuickActions();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Confirm selection
|
|
253
|
+
confirmBtn.onclick = () => {
|
|
254
|
+
currentIconType = tempType;
|
|
255
|
+
currentIconValue = tempValue;
|
|
256
|
+
selectedSearchDomain = tempSelectedDomain;
|
|
257
|
+
modal.style.display = 'none';
|
|
258
|
+
updateIconPreviewMini(serverName, serverConfig);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Cancel
|
|
262
|
+
cancelBtn.onclick = () => {
|
|
263
|
+
modal.style.display = 'none';
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
modal.style.display = 'flex';
|
|
267
|
+
searchInput.focus();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Save icon preference for a server (called after server is saved)
|
|
273
|
+
*/
|
|
274
|
+
function saveIconPreference(serverName) {
|
|
275
|
+
if (currentIconType === 'auto') {
|
|
276
|
+
removeIconPreference(serverName);
|
|
277
|
+
} else {
|
|
278
|
+
setIconPreference(serverName, currentIconType, currentIconValue);
|
|
279
|
+
}
|
|
280
|
+
// Clear cache so new preference takes effect
|
|
281
|
+
clearLogoCache(serverName);
|
|
282
|
+
}
|
|
283
|
+
|
|
17
284
|
export function showServerModal(serverName = null, serverConfig = null, loadClientServers, renderKanbanBoard, clientId = null, loadClientsFn) {
|
|
18
285
|
const modal = document.getElementById('serverModal');
|
|
19
286
|
const title = document.getElementById('modalTitle');
|
|
@@ -45,13 +312,11 @@ export function showServerModal(serverName = null, serverConfig = null, loadClie
|
|
|
45
312
|
|
|
46
313
|
// Set JSON editor content
|
|
47
314
|
if (!serverName && !serverConfig) {
|
|
48
|
-
// For new servers, show the expected format
|
|
315
|
+
// For new servers, show the expected config format (server name comes from form field)
|
|
49
316
|
document.getElementById('jsonEditor').value = JSON.stringify({
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"env": {}
|
|
54
|
-
}
|
|
317
|
+
"command": "npx",
|
|
318
|
+
"args": ["-y", "@example/mcp-server"],
|
|
319
|
+
"env": {}
|
|
55
320
|
}, null, 2);
|
|
56
321
|
} else {
|
|
57
322
|
document.getElementById('jsonEditor').value = JSON.stringify(serverConfig || {}, null, 2);
|
|
@@ -94,6 +359,9 @@ export function showServerModal(serverName = null, serverConfig = null, loadClie
|
|
|
94
359
|
btn.onclick = () => switchTab(btn.dataset.tab);
|
|
95
360
|
});
|
|
96
361
|
|
|
362
|
+
// Initialize icon editor
|
|
363
|
+
initIconEditor(serverName, serverConfig);
|
|
364
|
+
|
|
97
365
|
modal.style.display = 'flex';
|
|
98
366
|
document.getElementById('serverName').focus();
|
|
99
367
|
|
|
@@ -128,28 +396,35 @@ async function saveServer(originalName, loadClientServers, renderKanbanBoard, lo
|
|
|
128
396
|
try {
|
|
129
397
|
const jsonData = JSON.parse(document.getElementById('jsonEditor').value);
|
|
130
398
|
|
|
399
|
+
// Server name always comes from the form field
|
|
400
|
+
newServerName = document.getElementById('serverName').value;
|
|
401
|
+
if (!newServerName) {
|
|
402
|
+
alert('Server name cannot be empty. Please enter a name in the Server Name field.');
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
131
406
|
// Check if this is a new server (originalName is null)
|
|
132
407
|
if (!originalName) {
|
|
133
|
-
// For new servers
|
|
408
|
+
// For new servers, JSON should be just the config (not wrapped)
|
|
409
|
+
// But also support legacy wrapped format { "serverName": { config } } for backwards compatibility
|
|
134
410
|
const keys = Object.keys(jsonData);
|
|
135
411
|
if (keys.length === 0) {
|
|
136
|
-
alert('Server configuration cannot be empty.
|
|
412
|
+
alert('Server configuration cannot be empty.');
|
|
137
413
|
return;
|
|
138
414
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
415
|
+
|
|
416
|
+
// Check if this looks like a wrapped format (single key with object value containing command/args/env)
|
|
417
|
+
if (keys.length === 1 && typeof jsonData[keys[0]] === 'object' &&
|
|
418
|
+
(jsonData[keys[0]].command || jsonData[keys[0]].args || jsonData[keys[0]].env || jsonData[keys[0]].type)) {
|
|
419
|
+
// Legacy wrapped format - use the inner config
|
|
420
|
+
serverConfig = jsonData[keys[0]];
|
|
421
|
+
} else {
|
|
422
|
+
// New unwrapped format - use JSON directly as config
|
|
423
|
+
serverConfig = jsonData;
|
|
142
424
|
}
|
|
143
|
-
newServerName = keys[0];
|
|
144
|
-
serverConfig = jsonData[newServerName];
|
|
145
425
|
} else {
|
|
146
|
-
// For editing existing servers, use the
|
|
426
|
+
// For editing existing servers, use the JSON directly as config
|
|
147
427
|
serverConfig = jsonData;
|
|
148
|
-
newServerName = document.getElementById('serverName').value;
|
|
149
|
-
if (!newServerName) {
|
|
150
|
-
alert('Server name cannot be empty.');
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
428
|
}
|
|
154
429
|
} catch (e) {
|
|
155
430
|
alert('Invalid JSON format');
|
|
@@ -231,6 +506,9 @@ async function saveServer(originalName, loadClientServers, renderKanbanBoard, lo
|
|
|
231
506
|
if (!renameResponse.success) {
|
|
232
507
|
throw new Error('Failed to rename server');
|
|
233
508
|
}
|
|
509
|
+
// Migrate icon preference if server was renamed
|
|
510
|
+
const { migrateIconPreference } = await import('./iconPreferences.js');
|
|
511
|
+
migrateIconPreference(originalName, newServerName);
|
|
234
512
|
} catch (error) {
|
|
235
513
|
alert('Failed to rename server: ' + error.message);
|
|
236
514
|
return;
|
|
@@ -253,6 +531,9 @@ async function saveServer(originalName, loadClientServers, renderKanbanBoard, lo
|
|
|
253
531
|
throw new Error(`Failed to save server`);
|
|
254
532
|
}
|
|
255
533
|
|
|
534
|
+
// Save icon preference
|
|
535
|
+
saveIconPreference(serverToSaveName);
|
|
536
|
+
|
|
256
537
|
document.getElementById('serverModal').style.display = 'none';
|
|
257
538
|
await loadClientServers();
|
|
258
539
|
if (renderKanbanBoard) {
|
|
@@ -504,10 +785,7 @@ export async function deleteSelected(loadClientServers, renderKanbanBoard, loadC
|
|
|
504
785
|
}
|
|
505
786
|
|
|
506
787
|
export async function removeFromAll(serverName, loadClients, loadClientServers, loadClientsFn) {
|
|
507
|
-
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
|
|
788
|
+
// Note: Confirmation should be handled by the caller before calling this function
|
|
511
789
|
let removedCount = 0;
|
|
512
790
|
const errors = [];
|
|
513
791
|
|
|
@@ -533,11 +811,16 @@ export async function removeFromAll(serverName, loadClients, loadClientServers,
|
|
|
533
811
|
|
|
534
812
|
if (errors.length > 0) {
|
|
535
813
|
alert(`Removed "${serverName}" from ${removedCount} client(s).\nFailed for: ${errors.join(', ')}`);
|
|
814
|
+
} else if (removedCount === 0) {
|
|
815
|
+
alert(`Server "${serverName}" was not found in any clients.`);
|
|
536
816
|
} else {
|
|
817
|
+
alert(`Successfully removed "${serverName}" from ${removedCount} client(s).`);
|
|
537
818
|
}
|
|
538
819
|
|
|
539
820
|
loadClientsFn();
|
|
540
|
-
|
|
821
|
+
if (loadClientServers) {
|
|
822
|
+
await loadClientServers();
|
|
823
|
+
}
|
|
541
824
|
}
|
|
542
825
|
|
|
543
826
|
export const showAddServerToClientsModal = (serverName, serverConfig) => {
|
|
@@ -754,6 +1037,43 @@ export const showCopySingleEnvVarModal = async (serverName, envKey, envValue, so
|
|
|
754
1037
|
};
|
|
755
1038
|
|
|
756
1039
|
|
|
1040
|
+
export function showSelectClientModal(onClientSelected, loadClientsFn) {
|
|
1041
|
+
const modal = document.getElementById('selectClientModal');
|
|
1042
|
+
const selectClientList = document.getElementById('selectClientList');
|
|
1043
|
+
|
|
1044
|
+
// Populate client list as radio buttons (single selection)
|
|
1045
|
+
selectClientList.innerHTML = '';
|
|
1046
|
+
clients.forEach((client, index) => {
|
|
1047
|
+
const item = document.createElement('div');
|
|
1048
|
+
item.className = 'radio-item';
|
|
1049
|
+
item.innerHTML = `
|
|
1050
|
+
<input type="radio" name="selectedClient" id="selectClient-${client.id}" value="${client.id}" ${index === 0 ? 'checked' : ''}>
|
|
1051
|
+
<label for="selectClient-${client.id}">${client.name}</label>
|
|
1052
|
+
`;
|
|
1053
|
+
selectClientList.appendChild(item);
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
modal.style.display = 'flex';
|
|
1057
|
+
|
|
1058
|
+
const form = document.getElementById('selectClientForm');
|
|
1059
|
+
form.onsubmit = (e) => {
|
|
1060
|
+
e.preventDefault();
|
|
1061
|
+
const selectedRadio = document.querySelector('input[name="selectedClient"]:checked');
|
|
1062
|
+
if (!selectedRadio) {
|
|
1063
|
+
alert('Please select a client.');
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const selectedClientId = selectedRadio.value;
|
|
1067
|
+
modal.style.display = 'none';
|
|
1068
|
+
onClientSelected(selectedClientId);
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
// Setup cancel button
|
|
1072
|
+
document.getElementById('cancelSelectClient').onclick = () => {
|
|
1073
|
+
modal.style.display = 'none';
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
757
1077
|
export function showRemoteServerModal(loadClientServers, renderKanbanBoard, clientId = null, loadClientsFn) {
|
|
758
1078
|
const modal = document.getElementById('remoteServerModal');
|
|
759
1079
|
|