| No configuration entries found |
'
return
}
configKeys.forEach(key => {
const row = this.#createConfigRow(key)
tbody.appendChild(row)
})
}
/**
* Create a config table row
* @param {string} key
* @returns {HTMLTableRowElement}
*/
#createConfigRow(key) {
const value = this.#modifiedConfig[key]
const isModified = this.#modifiedKeys.has(key)
const isEditing = this.#editingKey === key
const valuesKey = `${key}.values`
const allowedValues = this.#originalConfig[valuesKey]
const row = document.createElement('tr')
row.dataset.key = key
row.style.cssText = `
border-bottom: 1px solid var(--sl-color-neutral-200);
background: ${isModified ? 'var(--sl-color-warning-50)' : 'white'};
`
row.addEventListener('mouseenter', () => {
if (!isModified) row.style.background = 'var(--sl-color-neutral-50)'
})
row.addEventListener('mouseleave', () => {
if (!isModified) row.style.background = 'white'
})
const nameCell = document.createElement('td')
nameCell.style.cssText = 'padding: 0.5rem; font-family: monospace; font-size: 0.85em; word-break: break-word;'
nameCell.textContent = key
row.appendChild(nameCell)
const valueCell = document.createElement('td')
valueCell.style.cssText = 'padding: 0.5rem;'
const isReadOnly = !isEditing && !isModified
const { container, setReadOnly } = this.#createValueEditor(key, value, allowedValues, isReadOnly)
valueCell.appendChild(container)
if (setReadOnly) {
requestAnimationFrame(() => setReadOnly(isReadOnly))
}
valueCell.addEventListener('dblclick', () => {
if (!isEditing && !isModified) {
this.#enableEditing(key)
}
})
row.appendChild(valueCell)
const actionsCell = document.createElement('td')
actionsCell.style.cssText = 'padding: 0.5rem; text-align: center;'
const editBtn = document.createElement('sl-icon-button')
if (isEditing || isModified) {
editBtn.setAttribute('name', 'check-circle')
editBtn.setAttribute('label', 'Save')
editBtn.style.cssText = 'color: var(--sl-color-success-600);'
editBtn.addEventListener('click', () => this.#saveValue(key))
} else {
editBtn.setAttribute('name', 'pencil')
editBtn.setAttribute('label', 'Edit')
editBtn.addEventListener('click', () => this.#enableEditing(key))
}
actionsCell.appendChild(editBtn)
if (isModified || isEditing) {
const resetBtn = document.createElement('sl-icon-button')
resetBtn.setAttribute('name', 'arrow-counterclockwise')
resetBtn.setAttribute('label', 'Reset')
resetBtn.addEventListener('click', () => this.#resetValue(key))
actionsCell.appendChild(resetBtn)
}
row.appendChild(actionsCell)
return row
}
/**
* Create value editor based on type and constraints
* @param {string} key
* @param {any} value
* @param {any[] | undefined} allowedValues
* @param {boolean} isReadOnly
* @returns {{container: HTMLElement, setReadOnly?: (readonly: boolean) => void}}
*/
#createValueEditor(key, value, allowedValues, isReadOnly = true) {
const container = document.createElement('div')
container.style.width = '100%'
if (allowedValues && Array.isArray(allowedValues)) {
const select = document.createElement('sl-select')
select.setAttribute('size', 'small')
select.value = value
select.disabled = isReadOnly
allowedValues.forEach(val => {
const option = document.createElement('sl-option')
option.value = val
option.textContent = val
select.appendChild(option)
})
if (!isReadOnly) {
select.addEventListener('sl-change', () => {
this.#handleValueChange(key, select.value)
})
}
container.appendChild(select)
return {
container,
setReadOnly: (readonly) => { select.disabled = readonly }
}
}
const actualType = typeof value
if (actualType === 'boolean') {
const checkbox = document.createElement('sl-checkbox')
checkbox.checked = Boolean(value)
checkbox.disabled = isReadOnly
if (!isReadOnly) {
checkbox.addEventListener('sl-change', () => {
this.#handleValueChange(key, checkbox.checked)
})
}
container.appendChild(checkbox)
return {
container,
setReadOnly: (readonly) => { checkbox.disabled = readonly }
}
} else if (actualType === 'number') {
const input = document.createElement('sl-input')
input.setAttribute('type', 'number')
input.setAttribute('size', 'small')
input.value = value != null ? String(value) : ''
if (!isReadOnly) {
input.addEventListener('sl-input', () => {
const numValue = parseFloat(input.value)
this.#handleValueChange(key, isNaN(numValue) ? null : numValue)
})
}
container.appendChild(input)
return {
container,
setReadOnly: (readonly) => {
input.readonly = readonly
this.#applyReadOnlyStyle(input, readonly)
}
}
} else if (this.#isStringArray(value)) {
const input = document.createElement('sl-input')
input.setAttribute('size', 'small')
input.value = this.#arrayToCommaSeparated(value)
input.style.fontFamily = 'monospace'
if (!isReadOnly) {
input.addEventListener('sl-input', () => {
try {
const parsed = this.#commaSeparatedToArray(input.value)
this.#handleValueChange(key, parsed)
} catch (e) {
this.#logger.error('Failed to parse array:', e)
}
})
}
container.appendChild(input)
return {
container,
setReadOnly: (readonly) => {
input.readonly = readonly
this.#applyReadOnlyStyle(input, readonly)
}
}
} else if (actualType === 'object') {
const input = document.createElement('sl-input')
input.setAttribute('size', 'small')
input.value = JSON.stringify(value)
input.style.fontFamily = 'monospace'
if (!isReadOnly) {
input.addEventListener('sl-input', () => {
this.#handleValueChange(key, input.value)
})
}
container.appendChild(input)
return {
container,
setReadOnly: (readonly) => {
input.readonly = readonly
this.#applyReadOnlyStyle(input, readonly)
}
}
} else {
const input = document.createElement('sl-input')
input.setAttribute('size', 'small')
input.value = value != null ? String(value) : ''
if (!isReadOnly) {
input.addEventListener('sl-input', () => {
this.#handleValueChange(key, input.value)
})
}
container.appendChild(input)
return {
container,
setReadOnly: (readonly) => {
input.readonly = readonly
this.#applyReadOnlyStyle(input, readonly)
}
}
}
}
/**
* @param {string} key
* @param {any} newValue
*/
#handleValueChange(key, newValue) {
this.#modifiedConfig[key] = newValue
if (JSON.stringify(newValue) !== JSON.stringify(this.#originalConfig[key])) {
this.#modifiedKeys.add(key)
} else {
this.#modifiedKeys.delete(key)
}
this.#updateRowState(key)
}
/**
* @param {string} key
*/
#updateRowState(key) {
const tbody = this.#dialogUi.configTable.configTableBody
const row = tbody.querySelector(`tr[data-key="${CSS.escape(key)}"]`)
if (!row) return
const isModified = this.#modifiedKeys.has(key)
row.style.background = isModified ? 'var(--sl-color-warning-50)' : 'white'
}
/**
* @param {string} key
*/
async #saveValue(key) {
try {
let valueToSave = this.#modifiedConfig[key]
const originalType = typeof this.#originalConfig[key]
if (originalType === 'object' && typeof valueToSave === 'string') {
try {
valueToSave = JSON.parse(valueToSave)
} catch (e) {
notify(`Invalid JSON for ${key}: ${e.message}`, 'danger', 'exclamation-octagon')
return
}
}
await this.getDependency('client').apiClient.configSet({ key, value: valueToSave })
this.#originalConfig[key] = valueToSave
this.#modifiedConfig[key] = valueToSave
this.#modifiedKeys.delete(key)
this.#editingKey = null
notify(`Saved ${key}`, 'success', 'check-circle')
this.#renderConfigList(this.#dialogUi.searchInput.value)
} catch (error) {
this.#logger.error('Failed to save config value:', error)
notify(`Failed to save ${key}`, 'danger', 'exclamation-octagon')
}
}
/**
* @param {string} key
*/
#resetValue(key) {
this.#modifiedConfig[key] = this.#originalConfig[key]
this.#modifiedKeys.delete(key)
this.#editingKey = null
this.#renderConfigList(this.#dialogUi.searchInput.value)
}
/**
* @param {string} key
*/
#enableEditing(key) {
this.#editingKey = key
this.#renderConfigList(this.#dialogUi.searchInput.value)
}
#handleSearch() {
const filterText = this.#dialogUi.searchInput.value
this.#renderConfigList(filterText)
}
// --- Utility helpers ---
/**
* @param {any[]} arr
* @returns {string}
*/
#arrayToCommaSeparated(arr) {
return arr.map(item => {
const str = String(item)
if (str.includes(' ') || str.includes(',')) {
return `"${str.replace(/"/g, '\\"')}"`
}
return str
}).join(', ')
}
/**
* @param {string} str
* @returns {any[]}
*/
#commaSeparatedToArray(str) {
const items = []
let current = ''
let inQuotes = false
let escaped = false
for (let i = 0; i < str.length; i++) {
const char = str[i]
if (escaped) { current += char; escaped = false; continue }
if (char === '\\') { escaped = true; continue }
if (char === '"') { inQuotes = !inQuotes; continue }
if (char === ',' && !inQuotes) {
if (current.trim()) items.push(current.trim())
current = ''
continue
}
current += char
}
if (current.trim()) items.push(current.trim())
return items
}
/**
* @param {any} value
* @returns {boolean}
*/
#isStringArray(value) {
return Array.isArray(value) && value.length > 0 && value.every(item => typeof item === 'string')
}
/**
* @param {HTMLElement} element
* @param {boolean} readonly
*/
#applyReadOnlyStyle(element, readonly) {
if (readonly) {
element.style.setProperty('--sl-input-border-width', '0')
element.style.setProperty('--sl-input-border-color', 'transparent')
element.style.setProperty('--sl-input-border-color-hover', 'transparent')
element.style.setProperty('--sl-input-border-color-focus', 'transparent')
element.style.setProperty('--sl-input-background-color', 'transparent')
element.style.setProperty('--sl-input-background-color-hover', 'transparent')
element.style.setProperty('--sl-input-background-color-focus', 'transparent')
element.style.setProperty('--sl-focus-ring-width', '0')
element.style.setProperty('--sl-focus-ring-color', 'transparent')
element.style.setProperty('--sl-input-focus-ring-width', '0')
element.style.setProperty('--sl-input-focus-ring-color', 'transparent')
element.style.cursor = 'default'
element.style.pointerEvents = 'none'
} else {
element.style.removeProperty('--sl-input-border-width')
element.style.removeProperty('--sl-input-border-color')
element.style.removeProperty('--sl-input-border-color-hover')
element.style.removeProperty('--sl-input-border-color-focus')
element.style.removeProperty('--sl-input-background-color')
element.style.removeProperty('--sl-input-background-color-hover')
element.style.removeProperty('--sl-input-background-color-focus')
element.style.removeProperty('--sl-focus-ring-width')
element.style.removeProperty('--sl-focus-ring-color')
element.style.removeProperty('--sl-input-focus-ring-width')
element.style.removeProperty('--sl-input-focus-ring-color')
element.style.cursor = ''
element.style.pointerEvents = ''
}
}
}
export default ConfigEditorPlugin