Feat: Add toggle switches for server props editor modal

Feat: Add search for settings in modal
Update: CSS
This commit is contained in:
MCHost
2025-06-26 02:19:29 -04:00
parent 591438bdb1
commit 728ea10e80
3 changed files with 649 additions and 299 deletions

View File

@ -501,78 +501,78 @@ document.addEventListener('DOMContentLoaded', () => {
const sftpBrowserSection = elements.sftpBrowserSection;
if (startBtn) {
if (status.toLowerCase() === 'running') {
startBtn.disabled = true;
startBtn.classList.add('disabled-btn');
} else {
startBtn.disabled = false;
startBtn.classList.remove('disabled-btn');
}
if (status.toLowerCase() === 'running') {
startBtn.disabled = true;
startBtn.classList.add('disabled-btn');
} else {
startBtn.disabled = false;
startBtn.classList.remove('disabled-btn');
}
}
if (status.toLowerCase() !== 'running') {
sections.forEach(section => {
if (section !== serverStatusSection) {
section.classList.add('hidden');
}
});
if (editPropertiesBtn) {
editPropertiesBtn.classList.add('hidden');
}
if (updateModsBtn) {
updateModsBtn.classList.add('hidden');
}
if (backupBtn) {
backupBtn.classList.add('hidden');
}
if (sftpBtn) {
sftpBtn.classList.add('hidden');
}
if (stopBtn) {
stopBtn.disabled = true;
stopBtn.classList.add('disabled-btn');
}
if (restartBtn) {
restartBtn.disabled = true;
restartBtn.classList.add('disabled-btn');
}
if (sftpBrowserSection) {
sftpBrowserSection.style.display = 'none';
}
if (!state.hasShownStartNotification) {
showNotification('Server is stopped. Click "Start" to enable all features.', 'error', 'server-stopped');
state.hasShownStartNotification = true;
sections.forEach(section => {
if (section !== serverStatusSection) {
section.classList.add('hidden');
}
});
if (editPropertiesBtn) {
editPropertiesBtn.classList.add('hidden');
}
if (updateModsBtn) {
updateModsBtn.classList.add('hidden');
}
if (backupBtn) {
backupBtn.classList.add('hidden');
}
if (sftpBtn) {
sftpBtn.classList.add('hidden');
}
if (stopBtn) {
stopBtn.disabled = true;
stopBtn.classList.add('disabled-btn');
}
if (restartBtn) {
restartBtn.disabled = true;
restartBtn.classList.add('disabled-btn');
}
if (sftpBrowserSection) {
sftpBrowserSection.style.display = 'none';
}
if (!state.hasShownStartNotification) {
showNotification('Server is stopped. Click "Start" to enable all features.', 'error', 'server-stopped');
state.hasShownStartNotification = true;
}
} else {
sections.forEach(section => {
section.classList.remove('hidden');
});
if (editPropertiesBtn) {
editPropertiesBtn.classList.remove('hidden');
}
if (updateModsBtn) {
updateModsBtn.classList.remove('hidden');
}
if (backupBtn) {
backupBtn.classList.remove('hidden');
}
if (sftpBtn) {
sftpBtn.classList.remove('hidden');
}
if (stopBtn) {
stopBtn.disabled = false;
stopBtn.classList.remove('disabled-btn');
}
if (restartBtn) {
restartBtn.disabled = false;
restartBtn.classList.remove('disabled-btn');
}
if (sftpBrowserSection) {
sftpBrowserSection.style.display = 'block';
}
state.hasShownStartNotification = false;
sections.forEach(section => {
section.classList.remove('hidden');
});
if (editPropertiesBtn) {
editPropertiesBtn.classList.remove('hidden');
}
if (updateModsBtn) {
updateModsBtn.classList.remove('hidden');
}
if (backupBtn) {
backupBtn.classList.remove('hidden');
}
if (sftpBtn) {
sftpBtn.classList.remove('hidden');
}
if (stopBtn) {
stopBtn.disabled = false;
stopBtn.classList.remove('disabled-btn');
}
if (restartBtn) {
restartBtn.disabled = false;
restartBtn.classList.remove('disabled-btn');
}
if (sftpBrowserSection) {
sftpBrowserSection.style.display = 'block';
}
state.hasShownStartNotification = false;
}
}
}
function updateDockerLogsUI(message) {
if (message.error) {
@ -1410,6 +1410,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
let displayProperties = {}; // Store original properties for filtering
function parseServerProperties(content) {
const properties = {};
const lines = content.split('\n');
@ -1424,50 +1426,174 @@ document.addEventListener('DOMContentLoaded', () => {
return properties;
}
function renderPropertiesFields(properties) {
function renderPropertiesFields(properties, filter = '') {
const fieldsContainer = elements.propertiesFields;
fieldsContainer.innerHTML = '';
Object.entries(properties).forEach(([key, value]) => {
// Create search box only if not already present
let searchContainer = fieldsContainer.querySelector('#searchContainer');
if (!searchContainer) {
searchContainer = document.createElement('div');
searchContainer.id = 'searchContainer';
searchContainer.className = 'mb-4';
searchContainer.style.display = 'block';
const searchLabel = document.createElement('label');
searchLabel.textContent = 'Search Properties';
searchLabel.className = 'block text-sm font-medium mb-1 text-white';
searchLabel.setAttribute('for', 'propertiesSearch');
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.id = 'propertiesSearch';
searchInput.placeholder = 'Search properties...';
searchInput.className = 'bg-gray-700 px-4 py-2 rounded text-white w-full';
searchInput.setAttribute('aria-label', 'Search server properties');
searchContainer.appendChild(searchLabel);
searchContainer.appendChild(searchInput);
fieldsContainer.appendChild(searchContainer);
// Add input event listener
searchInput.addEventListener('input', (e) => {
renderPropertiesList(displayProperties, e.target.value);
});
// Store reference to search input
elements.searchInput = searchInput;
}
// Create or update properties list container
let propertiesList = fieldsContainer.querySelector('#propertiesList');
if (!propertiesList) {
propertiesList = document.createElement('div');
propertiesList.id = 'propertiesList';
propertiesList.className = 'space-y-2';
fieldsContainer.appendChild(propertiesList);
}
renderPropertiesList(properties, filter);
// Add modal close handler
const closeButton = elements.editPropertiesModal.querySelector('.close-button');
if (closeButton && !closeButton.dataset.closeHandlerAdded) {
closeButton.addEventListener('click', () => {
elements.searchInput.value = '';
renderPropertiesList(displayProperties, '');
elements.editPropertiesModal.classList.add('hidden');
});
closeButton.dataset.closeHandlerAdded = 'true';
}
}
function renderPropertiesList(properties, filter = '') {
const propertiesList = elements.propertiesFields.querySelector('#propertiesList');
propertiesList.innerHTML = '';
// Filter properties based on search input
const filteredProperties = Object.entries(properties).filter(([key]) =>
key.toLowerCase().includes(filter.toLowerCase())
);
// Render filtered properties
filteredProperties.forEach(([key, value]) => {
if (filteredSettings.includes(key)) {
return;
}
console.log(`Rendering field for ${key}: ${value}`); // Debug log
const fieldDiv = document.createElement('div');
fieldDiv.className = 'flex items-center space-x-2';
fieldDiv.style.display = 'flex';
let inputType = 'text';
let isBoolean = value.toLowerCase() === 'true' || value.toLowerCase() === 'false';
if (isBoolean) {
inputType = 'checkbox';
inputType = 'switch';
} else if (/^\d+$/.test(value) && !isNaN(parseInt(value))) {
inputType = 'number';
}
const label = document.createElement('label');
label.textContent = key;
label.className = 'w-1/3 text-sm font-medium';
label.className = 'w-1/3 text-sm font-medium text-white';
label.setAttribute('for', `prop-${key}`);
const input = document.createElement('input');
input.id = `prop-${key}`;
input.name = key;
input.className = 'bg-gray-700 px-4 py-2 rounded text-white w-2/3';
if (inputType === 'switch') {
// Hidden text input for accessibility and form association
const hiddenInput = document.createElement('input');
hiddenInput.type = 'text';
hiddenInput.id = `prop-${key}`;
hiddenInput.name = key;
hiddenInput.value = value.toLowerCase();
hiddenInput.style.display = 'none';
if (inputType === 'checkbox') {
input.type = 'checkbox';
input.checked = value.toLowerCase() === 'true';
const switchContainer = document.createElement('div');
switchContainer.className = 'relative inline-block';
switchContainer.setAttribute('role', 'switch');
switchContainer.setAttribute('aria-checked', value.toLowerCase());
switchContainer.setAttribute('tabindex', '0');
switchContainer.dataset.name = key;
switchContainer.style.width = '40px';
switchContainer.style.height = '24px';
switchContainer.style.display = 'inline-block';
switchContainer.style.position = 'relative';
switchContainer.style.cursor = 'pointer';
switchContainer.style.zIndex = '10';
const switchTrack = document.createElement('div');
switchTrack.className = 'block w-full h-full rounded-full';
switchTrack.style.backgroundColor = value.toLowerCase() === 'true' ? '#10B981' : '#4B5563';
switchTrack.style.transition = 'background-color 0.2s ease-in-out';
const switchHandle = document.createElement('div');
switchHandle.className = 'absolute rounded-full bg-white';
switchHandle.style.width = '16px';
switchHandle.style.height = '16px';
switchHandle.style.top = '4px';
switchHandle.style.left = value.toLowerCase() === 'true' ? '20px' : '4px';
switchHandle.style.transition = 'left 0.2s ease-in-out';
switchHandle.style.position = 'absolute';
switchHandle.style.zIndex = '11';
// Handle click and keyboard events
const toggleSwitch = () => {
const currentValue = hiddenInput.value === 'true';
const newValue = !currentValue;
hiddenInput.value = newValue.toString();
switchContainer.setAttribute('aria-checked', newValue.toString());
switchTrack.style.backgroundColor = newValue ? '#10B981' : '#4B5563';
switchHandle.style.left = newValue ? '20px' : '4px';
};
switchContainer.addEventListener('click', toggleSwitch);
switchContainer.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleSwitch();
}
});
switchContainer.appendChild(switchTrack);
switchContainer.appendChild(switchHandle);
fieldDiv.appendChild(label);
fieldDiv.appendChild(hiddenInput);
fieldDiv.appendChild(switchContainer);
} else {
const input = document.createElement('input');
input.id = `prop-${key}`;
input.name = key;
input.className = 'bg-gray-700 px-4 py-2 rounded text-white w-2/3';
input.type = inputType;
input.value = value;
if (inputType === 'number') {
input.min = '0';
}
fieldDiv.appendChild(label);
fieldDiv.appendChild(input);
}
fieldDiv.appendChild(label);
fieldDiv.appendChild(input);
fieldsContainer.appendChild(fieldDiv);
propertiesList.appendChild(fieldDiv);
});
}
@ -1492,7 +1618,7 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
allProperties = parseServerProperties(response.content || '');
const displayProperties = Object.fromEntries(
displayProperties = Object.fromEntries(
Object.entries(allProperties).filter(([key]) => !filteredSettings.includes(key))
);
renderPropertiesFields(displayProperties);
@ -1509,15 +1635,18 @@ document.addEventListener('DOMContentLoaded', () => {
const key = `action-save-properties`;
const notification = showNotification('Saving server properties...', 'loading', key);
const properties = {};
const inputs = elements.propertiesFields.querySelectorAll('input');
const inputs = elements.propertiesFields.querySelectorAll('input:not(#propertiesSearch)');
// Collect modified properties
inputs.forEach(input => {
const key = input.name;
let value = input.type === 'checkbox' ? input.checked.toString() : input.value.trim();
let value = input.value.trim();
if (value !== '') {
properties[key] = value;
}
});
// Merge with allProperties to include all properties, even if not displayed
const fullProperties = { ...allProperties, ...properties };
const content = propertiesToString(fullProperties);
const response = await wsRequest('/server-properties', 'POST', { content });
@ -1525,6 +1654,11 @@ document.addEventListener('DOMContentLoaded', () => {
updateNotification(notification, `Failed to save server properties: ${response.error}`, 'error', key);
return;
}
// Clear search input and reset properties list
if (elements.searchInput) {
elements.searchInput.value = '';
renderPropertiesList(displayProperties, '');
}
elements.editPropertiesModal.classList.add('hidden');
updateNotification(notification, 'Server properties saved successfully', 'success', key);
} catch (error) {