aboutsummaryrefslogtreecommitdiffstats
diff options
-rw-r--r--src/main/resources/static/git-config-admin.js1453
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, '&amp;')
- .replace(/</g, '&lt;')
- .replace(/>/g, '&gt;')
- .replace(/"/g, '&quot;')
- .replace(/'/g, '&#039;');
- }
+ 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, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&#039;');
+ }
- _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);
});