diff options
Diffstat (limited to 'src/main/resources/static/git-config-admin.js')
-rw-r--r-- | src/main/resources/static/git-config-admin.js | 1453 |
1 files changed, 726 insertions, 727 deletions
diff --git a/src/main/resources/static/git-config-admin.js b/src/main/resources/static/git-config-admin.js index aac0374..846eae0 100644 --- a/src/main/resources/static/git-config-admin.js +++ b/src/main/resources/static/git-config-admin.js @@ -2,819 +2,818 @@ // Copyright (C) 2025 The Qt Company // -let pluginInstance = null; - -class RepoPicker extends HTMLElement { - constructor() { - super(); - this.attachShadow({ mode: 'open' }); - this._repos = []; - this._searchQuery = ''; - this._showResults = false; - this._debounceTimeout = null; - - this.shadowRoot.innerHTML = ` - <style> - :host { - display: block; - position: relative; - margin-bottom: 1em; - } - .results { - width: 100%; - max-height: 300px; - overflow-y: auto; - position: absolute; - z-index: 1000; - box-shadow: 0 2px 5px rgba(0,0,0,0.2); - margin-top: 4px; - background: white; - display: none; - } - .result-item { - padding: 8px; - cursor: pointer; - } - .result-item:hover { - background-color: #f0f0f0; - } - input { - width: 100%; - padding: 8px; - border: 1px solid #ccc; - border-radius: 4px; - } - </style> - <input type="text" placeholder="Search repository"> - <div class="results"></div> - `; - } - - connectedCallback() { - this.input = this.shadowRoot.querySelector('input'); - this.resultsContainer = this.shadowRoot.querySelector('.results'); - - this.input.addEventListener('input', this._handleInput.bind(this)); - this._loadAllRepos(); - } - - async _loadAllRepos() { - try { - const data = await pluginInstance.restApi().get('/projects/?n=1000'); - this._repos = Object.keys(data).filter(name => !name.startsWith('All-')); - this._filterResults(); - } catch (error) { - console.error('Failed to load repositories:', error); - } - } - - _handleInput(e) { - this._searchQuery = e.target.value; - this._filterResults(); - this._showResults = true; - this._renderResults(); - } - - _filterResults() { - const query = this._searchQuery.toLowerCase().trim(); - this.filteredRepos = this._repos.filter(name => - name.toLowerCase().includes(query) - ); - } - - _renderResults() { - this.resultsContainer.innerHTML = this.filteredRepos - .map(repo => `<div class="result-item">${repo}</div>`) - .join(''); - - this.resultsContainer.style.display = this._showResults && - this.filteredRepos.length > 0 ? 'block' : 'none'; - - this.resultsContainer.querySelectorAll('.result-item').forEach(item => { - item.addEventListener('mousedown', (e) => this._selectRepo(e.target.textContent)); - }); - } - - _selectRepo(repo) { - this.input.value = repo; - this._showResults = false; - this._renderResults(); - this.dispatchEvent(new CustomEvent('repo-selected', { - detail: { value: repo } - })); - } -} - -customElements.define('repo-picker', RepoPicker); - -class GitConfigMain extends HTMLElement { - constructor() { - super(); - this.attachShadow({ mode: 'open' }); - this._view = 'global'; - this.selectedRepo = null; - this.errorMessage = null; - - this.shadowRoot.innerHTML = ` - <style> - :host { - display: block; - padding: 1em; - max-width: 800px; - } - .tab-buttons { - display: flex; - margin-bottom: 1em; - } - .tab-button { - padding: 10px 20px; - cursor: pointer; - border: 1px solid #ccc; - background: #f0f0f0; - } - .tab-button.active { - background: #fff; - border-bottom: 1px solid transparent; - } - .error-message { - color: #721c24; - background-color: #f8d7da; - padding: 1em; - margin: 1em 0; - border-radius: 4px; - display: none; - } - .error-visible { - display: block; - } - </style> - <div class="tab-buttons"> - <button class="tab-button active" data-view="global">Global Config</button> - <button class="tab-button" data-view="repo">Repository Config</button> - </div> - <div class="error-message"></div> - <div id="globalView" class="view"> - <git-config-editor></git-config-editor> - </div> - <div id="repoView" class="view" style="display: none;"> - <repo-picker></repo-picker> - <git-config-editor></git-config-editor> - </div> - `; - } - - connectedCallback() { - this.tabButtons = this.shadowRoot.querySelectorAll('.tab-button'); - this.repoPicker = this.shadowRoot.querySelector('repo-picker'); - this.editors = this.shadowRoot.querySelectorAll('git-config-editor'); - this._loadConfig(); - - this.tabButtons.forEach(button => { - button.addEventListener('click', () => this._switchView(button.dataset.view)); - }); - - this.repoPicker.addEventListener('repo-selected', (e) => { - this.selectedRepo = e.detail.value; - this._loadConfig(); - }); - - this.editors.forEach(editor => { - editor.addEventListener('config-save', (e) => this._handleSaveConfig(e.detail.config)); - }); - } - - _showError(message) { - const tabEl = this.shadowRoot.querySelector('.tab-button'); - const errorEl = this.shadowRoot.querySelector('.error-message'); - errorEl.textContent = message; - errorEl.classList.add('error-visible'); - - // Scroll to top element - tabEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); - - // Auto-hide after 10 seconds - clearTimeout(this._errorTimeout); - this._errorTimeout = setTimeout(() => { - errorEl.classList.remove('error-visible'); - }, 10000); - } - - _switchView(view) { - this._view = view; - this.tabButtons.forEach(button => { - const active = button.dataset.view === view; - button.classList.toggle('active', active); - }); - this.shadowRoot.getElementById('globalView').style.display = - view === 'global' ? 'block' : 'none'; - this.shadowRoot.getElementById('repoView').style.display = - view === 'repo' ? 'block' : 'none'; - // If switched to repo view, clear selected repo - if (view === 'repo') { - this.selectedRepo = null; - this.repoPicker.input.value = ''; - this.editors.forEach(editor => editor.content = ''); - // Hide the editor by default - this.editors.forEach(editor => editor.style.display = 'none'); - return; - } - this._loadConfig(); - } - - async _loadConfig() { - const url = `/a/config/server/gitconfig${this._view === 'repo' && this.selectedRepo - ? `?repo=${encodeURIComponent(this.selectedRepo)}` - : '' - }`; - - try { - const response = await fetch(url); - if (!response.ok) throw new Error('Failed to load config'); - const content = await response.text(); - // decode base64 content - const decodedContent = atob(content); - this.editors.forEach(editor => editor.content = decodedContent); - this.editors.forEach(editor => editor.style.display = 'block'); - } catch (error) { - this._showError(error.message); - } - } +console.log('Git Config Admin plugin started'); +Gerrit.install(plugin => { + const admin = plugin.admin(); + admin.addMenuLink("Git Config", "/x/gerrit-plugin-gitconfig/gitconfig", "gerrit-plugin-gitconfig-manageGitConfig"); + plugin.screen("gitconfig", "git-config-main"); - async _handleSaveConfig(config) { - if (!config?.trim()) { - this._showError('Empty configuration content'); - return; - } + class RepoPicker extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this._repos = []; + this._searchQuery = ''; + this._showResults = false; + this._debounceTimeout = null; - try { - const url = `/config/server/gitconfig${this._view === 'repo' && this.selectedRepo - ? `?repo=${encodeURIComponent(this.selectedRepo)}` - : '' - }`; - // encode base64 content - const encodedConfig = btoa(config); - const response = await pluginInstance.restApi().put(url, encodedConfig); - const successText = await response; - this.editors.forEach(editor => editor.showSuccess(successText)); - } catch (error) { - this._showError(error.message || 'Error saving configuration'); - } - } -} - -customElements.define('git-config-main', GitConfigMain); - -// Load Prism.js for syntax highlighting -const prismLoader = document.createElement('script'); -prismLoader.src = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js'; -prismLoader.onload = () => { - const gitGrammar = document.createElement('script'); - gitGrammar.src = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-git.min.js'; - document.head.appendChild(gitGrammar); -}; -document.head.appendChild(prismLoader); - -class GitConfigEditor extends HTMLElement { - constructor() { - super(); - this.attachShadow({ mode: 'open' }); - this._content = ''; - this._sections = []; - this._validationError = null; - this._highlightTimeout = null; - - this.shadowRoot.innerHTML = ` + this.shadowRoot.innerHTML = ` <style> :host { display: block; position: relative; - margin: 1em 0; + margin-bottom: 1em; } - .editor-container { - border: 1px solid #ccc; - border-radius: 4px; - padding: 1em; - background: #f8f9fa; - } - .section { - margin-bottom: 2em; - border: 1px solid #dee2e6; - border-radius: 4px; + .results { + width: 100%; + max-height: 300px; + overflow-y: auto; + position: absolute; + z-index: 1000; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + margin-top: 4px; background: white; + display: none; } - .section-header { - padding: 12px; - background: #e9ecef; - border-bottom: 1px solid #dee2e6; - cursor: pointer; - display: flex; - align-items: center; - font-weight: bold; - } - .section-title { - flex-grow: 1; - color: #2c3e50; - } - .toggle-icon { - font-size: 1.2em; - margin-left: 8px; - } - .variable-container { - padding: 12px; - } - .variable-row { - display: flex; - align-items: center; - margin-bottom: 8px; + .result-item { padding: 8px; - background: #f8f9fa; - border-radius: 4px; - } - .variable-label { - flex: 0 0 300px; - font-family: monospace; - color: #6c757d; - padding-right: 16px; - border-right: 1px solid #dee2e6; + cursor: pointer; } - .variable-value { - flex-grow: 1; - padding-left: 16px; + .result-item:hover { + background-color: #f0f0f0; } input { width: 100%; - padding: 6px 12px; - border: 1px solid #ced4da; - border-radius: 4px; - font-family: monospace; - } - button { - background: #4CAF50; - color: white; - border: none; - padding: 8px 16px; + padding: 8px; + border: 1px solid #ccc; border-radius: 4px; - cursor: pointer; - margin-top: 16px; - } - .section-controls { - display: flex; - gap: 8px; - align-items: center; } + </style> + <input type="text" placeholder="Search repository"> + <div class="results"></div> + `; + } - .icon-btn { - background: none; - border: none; - cursor: pointer; - padding: 4px; - font-size: 16px; - opacity: 0.7; - transition: opacity 0.2s; - } + connectedCallback() { + this.input = this.shadowRoot.querySelector('input'); + this.resultsContainer = this.shadowRoot.querySelector('.results'); - .icon-btn:hover { - opacity: 1; - } + this.input.addEventListener('input', this._handleInput.bind(this)); + this._loadAllRepos(); + } - .variable-key { - width: 90%; - background: transparent; - border: 1px solid #ddd; - padding: 4px; - } + async _loadAllRepos() { + try { + const data = await plugin.restApi().get('/projects/?n=1000'); + this._repos = Object.keys(data).filter(name => !name.startsWith('All-')); + this._filterResults(); + } catch (error) { + console.error('Failed to load repositories:', error); + } + } - .add-section-btn { - width: 100%; - padding: 12px; - margin-top: 16px; - background:rgb(41, 100, 163); - border: 1px solid #ced4da; - cursor: pointer; - transition: background 0.2s; - } + _handleInput(e) { + this._searchQuery = e.target.value; + this._filterResults(); + this._showResults = true; + this._renderResults(); + } - .add-section-btn:hover { - background:rgb(35, 83, 135); - } + _filterResults() { + const query = this._searchQuery.toLowerCase().trim(); + this.filteredRepos = this._repos.filter(name => + name.toLowerCase().includes(query) + ); + } + + _renderResults() { + this.resultsContainer.innerHTML = this.filteredRepos + .map(repo => `<div class="result-item">${repo}</div>`) + .join(''); - .variable-value { + this.resultsContainer.style.display = this._showResults && + this.filteredRepos.length > 0 ? 'block' : 'none'; + + this.resultsContainer.querySelectorAll('.result-item').forEach(item => { + item.addEventListener('mousedown', (e) => this._selectRepo(e.target.textContent)); + }); + } + + _selectRepo(repo) { + this.input.value = repo; + this._showResults = false; + this._renderResults(); + this.dispatchEvent(new CustomEvent('repo-selected', { + detail: { value: repo } + })); + } + } + + customElements.define('repo-picker', RepoPicker); + + class GitConfigMain extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this._view = 'global'; + this.selectedRepo = null; + this.errorMessage = null; + + this.shadowRoot.innerHTML = ` + <style> + :host { + display: block; + padding: 1em; + max-width: 800px; + } + .tab-buttons { display: flex; - gap: 8px; - align-items: center; + margin-bottom: 1em; } - .section-name { + .tab-button { + padding: 10px 20px; cursor: pointer; - } - .section-edit { - font-family: monospace; - width: 80%; - padding: 2px 4px; border: 1px solid #ccc; - border-radius: 3px; + background: #f0f0f0; } - .edit-section { - order: -1; /* Move edit button first in controls */ + .tab-button.active { + background: #fff; + border-bottom: 1px solid transparent; } - .confirm-dialog { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: white; - padding: 2em; - border-radius: 8px; - box-shadow: 0 0 20px rgba(0,0,0,0.2); - z-index: 10000; - } - .dialog-buttons { - margin-top: 1em; - display: flex; - gap: 1em; - justify-content: flex-end; - } - .dialog-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0,0,0,0.5); - z-index: 9999; - } - .save-container { - display: flex; - align-items: center; - gap: 1rem; - margin-top: 16px; - } - - .success-message { - animation: fadeIn 0.5s; - color: #155724; - background-color: #d4edda; - width: 100%; - text-align: center; - padding: 0.5em 1em; + .error-message { + color: #721c24; + background-color: #f8d7da; + padding: 1em; + margin: 1em 0; border-radius: 4px; display: none; - margin: 0; /* Remove previous margin */ - font-size: 0.9em; - } - - .success-visible { - display: inline-block; /* Changed from block */ } - - /* Remove the margin from the button */ - #saveButton { - min-width: 20%; - margin: 0; - } - @keyframes fadeIn { - 0% { opacity: 0; } - 100% { opacity: 1; } - } - @keyframes fadeOut { - 0% { opacity: 1; } - 100% { opacity: 0; } + .error-visible { + display: block; } </style> - <div class="editor-container"> - <div class="sections-container"></div> - <div class="confirm-dialog" style="display: none;"> - <p class="dialog-message"></p> - <div class="dialog-buttons"> - <button class="cancel-btn">Cancel</button> - <button class="confirm-btn">Delete</button> - </div> + <div class="tab-buttons"> + <button class="tab-button active" data-view="global">Global Config</button> + <button class="tab-button" data-view="repo">Repository Config</button> + </div> + <div class="error-message"></div> + <div id="globalView" class="view"> + <git-config-editor></git-config-editor> </div> - <div class="dialog-overlay" style="display: none;"></div> - <div class="save-container"> - <button id="saveButton" type="button">Save Changes</button> - <div class="success-message"></div> + <div id="repoView" class="view" style="display: none;"> + <repo-picker></repo-picker> + <git-config-editor></git-config-editor> </div> - </div> `; - } + } - connectedCallback() { - this.sectionsContainer = this.shadowRoot.querySelector('.sections-container'); - this.saveButton = this.shadowRoot.getElementById('saveButton'); - this.saveButton.addEventListener('click', this._save.bind(this)); - } + connectedCallback() { + this.tabButtons = this.shadowRoot.querySelectorAll('.tab-button'); + this.repoPicker = this.shadowRoot.querySelector('repo-picker'); + this.editors = this.shadowRoot.querySelectorAll('git-config-editor'); + this._loadConfig(); - set content(value) { - this._content = value; - this._sections = this._parseConfig(); - this._renderSections(); - } + this.tabButtons.forEach(button => { + button.addEventListener('click', () => this._switchView(button.dataset.view)); + }); - get content() { - return this._sectionsToConfig(); - } + this.repoPicker.addEventListener('repo-selected', (e) => { + this.selectedRepo = e.detail.value; + this._loadConfig(); + }); - _parseConfig() { - const sections = []; - let currentSection = null; - let continuation = false; - - this._content.split('\n').forEach((line, index) => { - const hasContinuation = line.endsWith('\\'); - line = line.replace(/\\$/, '').trim(); - if (continuation) { - if (currentSection?.variables.length) { - currentSection.variables[currentSection.variables.length - 1].value += ' ' + line; - } - continuation = hasContinuation; - return; - } + this.editors.forEach(editor => { + editor.addEventListener('config-save', (e) => this._handleSaveConfig(e.detail.config)); + }); + } - const sectionMatch = line.match(/^\s*\[([^ "\]]+)(?: +"((?:[^"\\]|\\.)*)")?\]\s*$/i); - if (sectionMatch) { - currentSection = { - main: sectionMatch[1], - subsection: sectionMatch[2] ? sectionMatch[2].replace(/\\(.)/g, '$1') : null, // Unescape quotes - rawName: line.trim(), - variables: [], - collapsed: false, - lineNumber: index + 1 - }; - sections.push(currentSection); - } else if (currentSection) { - const variableMatch = line.match(/^\s*([^=;#]+?)\s*=\s*(.*?)\s*$/); - if (variableMatch) { - currentSection.variables.push({ - key: variableMatch[1].trim(), - value: variableMatch[2].trim(), - lineNumber: index + 1 - }); - } + _showError(message) { + const tabEl = this.shadowRoot.querySelector('.tab-button'); + const errorEl = this.shadowRoot.querySelector('.error-message'); + errorEl.textContent = message; + errorEl.classList.add('error-visible'); + + // Scroll to top element + tabEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); + + // Auto-hide after 10 seconds + clearTimeout(this._errorTimeout); + this._errorTimeout = setTimeout(() => { + errorEl.classList.remove('error-visible'); + }, 10000); + } + + _switchView(view) { + this._view = view; + this.tabButtons.forEach(button => { + const active = button.dataset.view === view; + button.classList.toggle('active', active); + }); + this.shadowRoot.getElementById('globalView').style.display = + view === 'global' ? 'block' : 'none'; + this.shadowRoot.getElementById('repoView').style.display = + view === 'repo' ? 'block' : 'none'; + // If switched to repo view, clear selected repo + if (view === 'repo') { + this.selectedRepo = null; + // clear the repo picker input + this.repoPicker.input.value = ''; + // clear editor content + this.editors.forEach(editor => editor.content = ''); + // Hide the editor by default + this.editors.forEach(editor => editor.style.display = 'none'); + return; } - continuation = hasContinuation; - }); + this._loadConfig(); + } - return sections; - } + async _loadConfig() { + const url = `/a/config/server/gitconfig${this._view === 'repo' && this.selectedRepo + ? `?repo=${encodeURIComponent(this.selectedRepo)}` + : '' + }`; - confirmModal(message) { - return new Promise((resolve) => { - const dialog = this.shadowRoot.querySelector('.confirm-dialog'); - const overlay = this.shadowRoot.querySelector('.dialog-overlay'); - const messageEl = dialog.querySelector('.dialog-message'); - const cancelBtn = dialog.querySelector('.cancel-btn'); - const confirmBtn = dialog.querySelector('.confirm-btn'); - - messageEl.textContent = message; - dialog.style.display = 'block'; - overlay.style.display = 'block'; - - const cleanUp = () => { - dialog.style.display = 'none'; - overlay.style.display = 'none'; - }; - - const handleConfirm = () => { - cleanUp(); - resolve(true); - }; - - const handleCancel = () => { - cleanUp(); - resolve(false); - }; - - confirmBtn.addEventListener('click', handleConfirm); - cancelBtn.addEventListener('click', handleCancel); - - // One-time listeners to prevent multiple registrations - const onceOptions = { once: true }; - overlay.addEventListener('click', handleCancel, onceOptions); - }); - } + try { + const response = await fetch(url); + if (!response.ok) throw new Error('Failed to load config'); + const content = await response.text(); + // decode base64 content + const decodedContent = atob(content); + this.editors.forEach(editor => editor.content = decodedContent); + this.editors.forEach(editor => editor.style.display = 'block'); + } catch (error) { + this._showError(error.message); + } + } - showSuccess(message) { - const successEl = this.shadowRoot.querySelector('.success-message'); - successEl.textContent = message; - successEl.classList.add('success-visible'); + async _handleSaveConfig(config) { + if (!config?.trim()) { + this._showError('Empty configuration content'); + return; + } - clearTimeout(this._successTimeout); - this._successTimeout = setTimeout(() => { - successEl.classList.remove('success-visible'); - }, 3000); + try { + const url = `/config/server/gitconfig${this._view === 'repo' && this.selectedRepo + ? `?repo=${encodeURIComponent(this.selectedRepo)}` + : '' + }`; + // encode base64 content + const encodedConfig = btoa(config); + const response = await plugin.restApi().put(url, encodedConfig); + const successText = await response; + this.editors.forEach(editor => editor.showSuccess(successText)); + } catch (error) { + this._showError(error.message || 'Error saving configuration'); + } + } } - _renderSections() { - this.sectionsContainer.innerHTML = ''; - this._sections.forEach((section, sectionIndex) => { - const sectionEl = document.createElement('div'); - sectionEl.className = 'section'; - sectionEl.setAttribute('data-line', section.lineNumber); - - const header = document.createElement('div'); - header.className = 'section-header'; - header.innerHTML = ` - <div class="section-title"> - <span class="section-name">[${section.main}${section.subsection ? ` "${section.subsection.replace(/"/g, '\\"')}"` : ''}]</span> - <button class="icon-btn edit-section" title="Rename section">✏️</button> - <div class="section-edit" style="display: none"> - <div class="edit-controls"> - <input type="text" class="section-main" value="${this._escapeHtml(section.main || '')}" placeholder="Section"> - <input type="text" class="section-sub" value="${this._escapeHtml(section.subsection || '')}" placeholder="Subsection (optional)"> - <div class="edit-buttons"> - </div> - <button class="icon-btn confirm-edit" title="Save changes">✅</button> - <button class="icon-btn cancel-edit" title="Cancel edit">❌</button> + customElements.define('git-config-main', GitConfigMain); + + // Load Prism.js for syntax highlighting + const prismLoader = document.createElement('script'); + prismLoader.src = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js'; + prismLoader.onload = () => { + const gitGrammar = document.createElement('script'); + gitGrammar.src = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-git.min.js'; + document.head.appendChild(gitGrammar); + }; + document.head.appendChild(prismLoader); + + class GitConfigEditor extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this._content = ''; + this._sections = []; + this._validationError = null; + this._highlightTimeout = null; + + this.shadowRoot.innerHTML = ` + <style> + :host { + display: block; + position: relative; + margin: 1em 0; + } + .editor-container { + border: 1px solid #ccc; + border-radius: 4px; + padding: 1em; + background: #f8f9fa; + } + .section { + margin-bottom: 2em; + border: 1px solid #dee2e6; + border-radius: 4px; + background: white; + } + .section-header { + padding: 12px; + background: #e9ecef; + border-bottom: 1px solid #dee2e6; + cursor: pointer; + display: flex; + align-items: center; + font-weight: bold; + } + .section-title { + flex-grow: 1; + color: #2c3e50; + } + .toggle-icon { + font-size: 1.2em; + margin-left: 8px; + } + .variable-container { + padding: 12px; + } + .variable-row { + display: flex; + align-items: center; + margin-bottom: 8px; + padding: 8px; + background: #f8f9fa; + border-radius: 4px; + } + .variable-label { + flex: 0 0 300px; + font-family: monospace; + color: #6c757d; + padding-right: 16px; + border-right: 1px solid #dee2e6; + } + .variable-value { + flex-grow: 1; + padding-left: 16px; + } + input { + width: 100%; + padding: 6px 12px; + border: 1px solid #ced4da; + border-radius: 4px; + font-family: monospace; + } + button { + background: #4CAF50; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + margin-top: 16px; + } + .section-controls { + display: flex; + gap: 8px; + align-items: center; + } + + .icon-btn { + background: none; + border: none; + cursor: pointer; + padding: 4px; + font-size: 16px; + opacity: 0.7; + transition: opacity 0.2s; + } + + .icon-btn:hover { + opacity: 1; + } + + .variable-key { + width: 90%; + background: transparent; + border: 1px solid #ddd; + padding: 4px; + } + + .add-section-btn { + width: 100%; + padding: 12px; + margin-top: 16px; + background:rgb(41, 100, 163); + border: 1px solid #ced4da; + cursor: pointer; + transition: background 0.2s; + } + + .add-section-btn:hover { + background:rgb(35, 83, 135); + } + + .variable-value { + display: flex; + gap: 8px; + align-items: center; + } + .section-name { + cursor: pointer; + } + .section-edit { + font-family: monospace; + width: 80%; + padding: 2px 4px; + border: 1px solid #ccc; + border-radius: 3px; + } + .edit-section { + order: -1; /* Move edit button first in controls */ + } + .confirm-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + padding: 2em; + border-radius: 8px; + box-shadow: 0 0 20px rgba(0,0,0,0.2); + z-index: 10000; + } + .dialog-buttons { + margin-top: 1em; + display: flex; + gap: 1em; + justify-content: flex-end; + } + .dialog-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.5); + z-index: 9999; + } + .save-container { + display: flex; + align-items: center; + gap: 1rem; + margin-top: 16px; + } + + .success-message { + animation: fadeIn 0.5s; + color: #155724; + background-color: #d4edda; + width: 100%; + text-align: center; + padding: 0.5em 1em; + border-radius: 4px; + display: none; + margin: 0; /* Remove previous margin */ + font-size: 0.9em; + } + + .success-visible { + display: inline-block; /* Changed from block */ + } + + /* Remove the margin from the button */ + #saveButton { + min-width: 20%; + margin: 0; + } + @keyframes fadeIn { + 0% { opacity: 0; } + 100% { opacity: 1; } + } + @keyframes fadeOut { + 0% { opacity: 1; } + 100% { opacity: 0; } + } + </style> + <div class="editor-container"> + <div class="sections-container"></div> + <div class="confirm-dialog" style="display: none;"> + <p class="dialog-message"></p> + <div class="dialog-buttons"> + <button class="cancel-btn">Cancel</button> + <button class="confirm-btn">Delete</button> </div> </div> - </div> - <div class="section-controls"> - <button class="icon-btn edit-section" title="Rename section" style="display: none">✏️</button> - <button class="icon-btn delete-section" title="Delete section">🗑️</button> - <button class="icon-btn add-variable" title="Add variable">➕</button> - <span class="toggle-icon">${section.collapsed ? '+' : '-'}</span> - </div> - `; - - const nameDisplay = header.querySelector('.section-name'); - const nameInput = header.querySelector('.section-edit'); - const editBtn = header.querySelector('.edit-section'); - - editBtn.addEventListener('click', (e) => { - e.stopPropagation(); - nameDisplay.style.display = 'none'; - nameInput.style.display = 'block'; - header.querySelector('.edit-section').style.display = 'none'; // Hide edit button - }); + <div class="dialog-overlay" style="display: none;"></div> + <div class="save-container"> + <button id="saveButton" type="button">Save Changes</button> + <div class="success-message"></div> + </div> + </div> + `; + } - const confirmEdit = header.querySelector('.confirm-edit'); - const cancelEdit = header.querySelector('.cancel-edit'); + connectedCallback() { + this.sectionsContainer = this.shadowRoot.querySelector('.sections-container'); + this.saveButton = this.shadowRoot.getElementById('saveButton'); + this.saveButton.addEventListener('click', this._save.bind(this)); + } - confirmEdit.addEventListener('click', (e) => { - e.stopPropagation(); - saveSectionName(); - header.querySelector('.edit-section').style.display = 'inline-block'; - }); + set content(value) { + this._content = value; + this._sections = this._parseConfig(); + this._renderSections(); + } - cancelEdit.addEventListener('click', (e) => { - e.stopPropagation(); - nameDisplay.style.display = 'inline-block'; - nameInput.style.display = 'none'; - header.querySelector('.edit-section').style.display = 'inline-block'; - }); + get content() { + return this._sectionsToConfig(); + } - const saveSectionName = () => { - const mainInput = header.querySelector('.section-main'); - const subInput = header.querySelector('.section-sub'); - const newMain = mainInput.value.trim(); - const newSub = subInput.value.trim().replace(/"/g, ''); // Prevent manual quoting + _parseConfig() { + const sections = []; + let currentSection = null; + let continuation = false; - if (newMain && (newMain !== section.main || newSub !== (section.subsection || ''))) { - section.main = newMain; - section.subsection = newSub || null; - section.rawName = `[${section.main}${section.subsection ? ` "${section.subsection.replace(/(["\\])/g, '\\$1')}"` : ''}]`; - this._content = this._sectionsToConfig(); - this._renderSections(); + this._content.split('\n').forEach((line, index) => { + const hasContinuation = line.endsWith('\\'); + line = line.replace(/\\$/, '').trim(); + if (continuation) { + if (currentSection?.variables.length) { + currentSection.variables[currentSection.variables.length - 1].value += ' ' + line; + } + continuation = hasContinuation; + return; } - nameDisplay.style.display = 'inline-block'; - nameInput.style.display = 'none'; - }; - - header.querySelector('.delete-section').addEventListener('click', async (e) => { - e.stopPropagation(); - const confirmed = await this.confirmModal(`Are you sure you want to delete the entire [${section.main}${section.subsection ? '.' + section.subsection : ''}] section?`); - if (confirmed) { - this._sections.splice(sectionIndex, 1); - this._content = this._sectionsToConfig(); - this._renderSections(); + const sectionMatch = line.match(/^\s*\[([^ "\]]+)(?: +"((?:[^"\\]|\\.)*)")?\]\s*$/i); + if (sectionMatch) { + currentSection = { + main: sectionMatch[1], + subsection: sectionMatch[2] ? sectionMatch[2].replace(/\\(.)/g, '$1') : null, // Unescape quotes + rawName: line.trim(), + variables: [], + collapsed: false, + lineNumber: index + 1 + }; + sections.push(currentSection); + } else if (currentSection) { + const variableMatch = line.match(/^\s*([^=;#]+?)\s*=\s*(.*?)\s*$/); + if (variableMatch) { + currentSection.variables.push({ + key: variableMatch[1].trim(), + value: variableMatch[2].trim(), + lineNumber: index + 1 + }); + } } + continuation = hasContinuation; }); - header.querySelector('.add-variable').addEventListener('click', (e) => { - e.stopPropagation(); - section.variables.push({ key: '', value: '' }); - this._content = this._sectionsToConfig(); - this._renderSections(); - }); + return sections; + } + + confirmModal(message) { + return new Promise((resolve) => { + const dialog = this.shadowRoot.querySelector('.confirm-dialog'); + const overlay = this.shadowRoot.querySelector('.dialog-overlay'); + const messageEl = dialog.querySelector('.dialog-message'); + const cancelBtn = dialog.querySelector('.cancel-btn'); + const confirmBtn = dialog.querySelector('.confirm-btn'); + + messageEl.textContent = message; + dialog.style.display = 'block'; + overlay.style.display = 'block'; + + const cleanUp = () => { + dialog.style.display = 'none'; + overlay.style.display = 'none'; + }; - header.addEventListener('click', () => { - section.collapsed = !section.collapsed; - variablesContainer.style.display = section.collapsed ? 'none' : 'block'; - header.querySelector('.toggle-icon').textContent = section.collapsed ? '+' : '-'; + const handleConfirm = () => { + cleanUp(); + resolve(true); + }; + + const handleCancel = () => { + cleanUp(); + resolve(false); + }; + + confirmBtn.addEventListener('click', handleConfirm); + cancelBtn.addEventListener('click', handleCancel); + + // One-time listeners to prevent multiple registrations + const onceOptions = { once: true }; + overlay.addEventListener('click', handleCancel, onceOptions); }); + } - const variablesContainer = document.createElement('div'); - variablesContainer.className = 'variable-container'; + showSuccess(message) { + const successEl = this.shadowRoot.querySelector('.success-message'); + successEl.textContent = message; + successEl.classList.add('success-visible'); - section.variables.forEach((variable, varIndex) => { - const row = document.createElement('div'); - row.className = 'variable-row'; - row.setAttribute('data-line', variable.lineNumber); + clearTimeout(this._successTimeout); + this._successTimeout = setTimeout(() => { + successEl.classList.remove('success-visible'); + }, 3000); + } - row.innerHTML = ` - <div class="variable-label"> - <input type="text" value="${this._escapeHtml(variable.key)}" - placeholder="Variable name" class="variable-key"> + _renderSections() { + this.sectionsContainer.innerHTML = ''; + this._sections.forEach((section, sectionIndex) => { + const sectionEl = document.createElement('div'); + sectionEl.className = 'section'; + sectionEl.setAttribute('data-line', section.lineNumber); + + const header = document.createElement('div'); + header.className = 'section-header'; + header.innerHTML = ` + <div class="section-title"> + <span class="section-name">[${section.main}${section.subsection ? ` "${section.subsection.replace(/"/g, '\\"')}"` : ''}]</span> + <button class="icon-btn edit-section" title="Rename section">✏️</button> + <div class="section-edit" style="display: none"> + <div class="edit-controls"> + <input type="text" class="section-main" value="${this._escapeHtml(section.main || '')}" placeholder="Section"> + <input type="text" class="section-sub" value="${this._escapeHtml(section.subsection || '')}" placeholder="Subsection (optional)"> + <div class="edit-buttons"> + </div> + <button class="icon-btn confirm-edit" title="Save changes">✅</button> + <button class="icon-btn cancel-edit" title="Cancel edit">❌</button> + </div> </div> - <div class="variable-value"> - <input type="text" value="${this._escapeHtml(variable.value)}"> - <button class="icon-btn delete-variable" title="Delete variable">🗑️</button> - </div> - `; + </div> + <div class="section-controls"> + <button class="icon-btn edit-section" title="Rename section" style="display: none">✏️</button> + <button class="icon-btn delete-section" title="Delete section">🗑️</button> + <button class="icon-btn add-variable" title="Add variable">➕</button> + <span class="toggle-icon">${section.collapsed ? '+' : '-'}</span> + </div> + `; - const keyInput = row.querySelector('.variable-key'); - keyInput.addEventListener('input', (e) => { - variable.key = e.target.value.trim(); - this._content = this._sectionsToConfig(); + const nameDisplay = header.querySelector('.section-name'); + const nameInput = header.querySelector('.section-edit'); + const editBtn = header.querySelector('.edit-section'); + + editBtn.addEventListener('click', (e) => { + e.stopPropagation(); + nameDisplay.style.display = 'none'; + nameInput.style.display = 'block'; + header.querySelector('.edit-section').style.display = 'none'; // Hide edit button }); - const valueInput = row.querySelector('input:not(.variable-key)'); - valueInput.addEventListener('input', (e) => { - variable.value = e.target.value.trim(); - this._content = this._sectionsToConfig(); + const confirmEdit = header.querySelector('.confirm-edit'); + const cancelEdit = header.querySelector('.cancel-edit'); + + confirmEdit.addEventListener('click', (e) => { + e.stopPropagation(); + saveSectionName(); + header.querySelector('.edit-section').style.display = 'inline-block'; + }); + + cancelEdit.addEventListener('click', (e) => { + e.stopPropagation(); + nameDisplay.style.display = 'inline-block'; + nameInput.style.display = 'none'; + header.querySelector('.edit-section').style.display = 'inline-block'; }); - row.querySelector('.delete-variable').addEventListener('click', async () => { - const confirmed = await this.confirmModal(`Delete variable "${variable.key}" from [${section.name}]?`); + const saveSectionName = () => { + const mainInput = header.querySelector('.section-main'); + const subInput = header.querySelector('.section-sub'); + const newMain = mainInput.value.trim(); + const newSub = subInput.value.trim().replace(/"/g, ''); // Prevent manual quoting + + if (newMain && (newMain !== section.main || newSub !== (section.subsection || ''))) { + section.main = newMain; + section.subsection = newSub || null; + section.rawName = `[${section.main}${section.subsection ? ` "${section.subsection.replace(/(["\\])/g, '\\$1')}"` : ''}]`; + this._content = this._sectionsToConfig(); + this._renderSections(); + } + + nameDisplay.style.display = 'inline-block'; + nameInput.style.display = 'none'; + }; + + header.querySelector('.delete-section').addEventListener('click', async (e) => { + e.stopPropagation(); + const confirmed = await this.confirmModal(`Are you sure you want to delete the entire [${section.main}${section.subsection ? '.' + section.subsection : ''}] section?`); if (confirmed) { - section.variables.splice(varIndex, 1); + this._sections.splice(sectionIndex, 1); this._content = this._sectionsToConfig(); this._renderSections(); } }); - variablesContainer.appendChild(row); - }); + header.querySelector('.add-variable').addEventListener('click', (e) => { + e.stopPropagation(); + section.variables.push({ key: '', value: '' }); + this._content = this._sectionsToConfig(); + this._renderSections(); + }); + + header.addEventListener('click', () => { + section.collapsed = !section.collapsed; + variablesContainer.style.display = section.collapsed ? 'none' : 'block'; + header.querySelector('.toggle-icon').textContent = section.collapsed ? '+' : '-'; + }); + + const variablesContainer = document.createElement('div'); + variablesContainer.className = 'variable-container'; + + section.variables.forEach((variable, varIndex) => { + const row = document.createElement('div'); + row.className = 'variable-row'; + row.setAttribute('data-line', variable.lineNumber); + + row.innerHTML = ` + <div class="variable-label"> + <input type="text" value="${this._escapeHtml(variable.key)}" + placeholder="Variable name" class="variable-key"> + </div> + <div class="variable-value"> + <input type="text" value="${this._escapeHtml(variable.value)}"> + <button class="icon-btn delete-variable" title="Delete variable">🗑️</button> + </div> + `; + + const keyInput = row.querySelector('.variable-key'); + keyInput.addEventListener('input', (e) => { + variable.key = e.target.value.trim(); + this._content = this._sectionsToConfig(); + }); - sectionEl.appendChild(header); - sectionEl.appendChild(variablesContainer); - this.sectionsContainer.appendChild(sectionEl); - }); - - const addSectionBtn = document.createElement('button'); - addSectionBtn.className = 'add-section-btn'; - addSectionBtn.textContent = '+ Add New Section'; - addSectionBtn.addEventListener('click', () => { - this._sections.push({ - name: 'new-section', - variables: [], - collapsed: false, - lineNumber: this._content.split('\n').length + 1 + const valueInput = row.querySelector('input:not(.variable-key)'); + valueInput.addEventListener('input', (e) => { + variable.value = e.target.value.trim(); + this._content = this._sectionsToConfig(); + }); + + row.querySelector('.delete-variable').addEventListener('click', async () => { + const confirmed = await this.confirmModal(`Delete variable "${variable.key}" from [${section.name}]?`); + if (confirmed) { + section.variables.splice(varIndex, 1); + this._content = this._sectionsToConfig(); + this._renderSections(); + } + }); + + variablesContainer.appendChild(row); + }); + + sectionEl.appendChild(header); + sectionEl.appendChild(variablesContainer); + this.sectionsContainer.appendChild(sectionEl); }); - this._content = this._sectionsToConfig(); - this._renderSections(); - }); - this.sectionsContainer.appendChild(addSectionBtn); - } - _escapeHtml(value) { - return value.replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } + const addSectionBtn = document.createElement('button'); + addSectionBtn.className = 'add-section-btn'; + addSectionBtn.textContent = '+ Add New Section'; + addSectionBtn.addEventListener('click', () => { + this._sections.push({ + name: 'new-section', + variables: [], + collapsed: false, + lineNumber: this._content.split('\n').length + 1 + }); + this._content = this._sectionsToConfig(); + this._renderSections(); + }); + this.sectionsContainer.appendChild(addSectionBtn); + } - _sectionsToConfig() { - return this._sections.map(section => - `[${section.main}${section.subsection ? ` "${section.subsection.replace(/(["\\])/g, '\\$1')}"` : ''}]\n${section.variables.map(v => `\t${v.key} = ${v.value}`).join('\n') - }` - ).join('\n\n') + "\n"; - } + _escapeHtml(value) { + return value.replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } - _isAbsolutePath(path) { - const cleanPath = path.replace(/^"+|"+$/g, ''); - return cleanPath.startsWith('/') || - /^[a-zA-Z]:[\\/]/.test(cleanPath) || - cleanPath.startsWith('\\\\'); - } + _sectionsToConfig() { + return this._sections.map(section => + `[${section.main}${section.subsection ? ` "${section.subsection.replace(/(["\\])/g, '\\$1')}"` : ''}]\n${section.variables.map(v => `\t${v.key} = ${v.value}`).join('\n') + }` + ).join('\n'); + } - _isUnsafeIncludePath(path) { - const cleanPath = path.replace(/^"+|"+$/g, ''); - return cleanPath.includes('..') || - /[<>|]/.test(cleanPath) || - this._isAbsolutePath(cleanPath); - } + _isAbsolutePath(path) { + const cleanPath = path.replace(/^"+|"+$/g, ''); + return cleanPath.startsWith('/') || + /^[a-zA-Z]:[\\/]/.test(cleanPath) || + cleanPath.startsWith('\\\\'); + } - _save() { - console.log('Saving config'); - if (this._validationError) { - return; + _isUnsafeIncludePath(path) { + const cleanPath = path.replace(/^"+|"+$/g, ''); + return cleanPath.includes('..') || + /[<>|]/.test(cleanPath) || + this._isAbsolutePath(cleanPath); } - this.dispatchEvent(new CustomEvent('config-save', { - detail: { config: this._content } - })); - } -} + _save() { + console.log('Saving config'); + if (this._validationError) { + return; + } -customElements.define('git-config-editor', GitConfigEditor); + this.dispatchEvent(new CustomEvent('config-save', { + detail: { config: this._content } + })); + } + } -console.log('Git Config Admin plugin started'); -Gerrit.install(plugin => { - pluginInstance = plugin; - const admin = plugin.admin(); - admin.addMenuLink("Git Config", "/x/gerrit-plugin-gitconfig/gitconfig", "gerrit-plugin-gitconfig-manageGitConfig"); - plugin.screen("gitconfig", "git-config-main"); + customElements.define('git-config-editor', GitConfigEditor); }); |