Added — Settings → Automations, pipeline visuel (API steps v7.0) :
- Cartes ordinees (trigger/condition/delay/action) : resume + edition
TYPÉE par kind/type (datalist evenements, 7 ops, 8 types d'action avec
leurs champs reels), ajout/edition/suppression/haut-bas via
POST/PUT/DELETE /workspace/automations[/steps]/...
- ✨ Convertir le JSON en pipeline (legacy → steps ordonnes) ; textareas
JSON masques des qu'un step existe.
- Gate E2E « editeur visuel de steps » : creation → edition → ajout →
carte « Action · webhook » sous CSP reel.
Fixed (trouve par le gate) — 51 expressions Alpine MULTI-INSTRUCTIONS
(`a=1; b()`) = interdites par le parseur CSP (une seule expression par
directive ; ';' = token inattendu) — INVISIBLE pour le scan par tokens :
- Conversion en methodes dans 11 fichiers : nav settings x8 (navTo),
menu section base x7 (closeAndSetCount/Move/...), parts+editeur x13
(setSharePerm, more*, markAndSave...), breadcrumb x5 (hover*/goClose),
library/local x6 (menus popup), board x3 (pickStatus/...), ctx-menu x2
(addTagAndClear), agent/card/gitea/workspaces x6.
- Scanner dedie scan_semi (inventaire ';' hors chaines) ajoute au lot.
Verifs : 39 templates Jinja parse OK · scan expressions = 0 incompatible
(4 faux positifs en chaines) · **E2E 8/8** · suite **1094/1094** ·
ruff OK · docs a jour
641 lines
24 KiB
JavaScript
641 lines
24 KiB
JavaScript
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data('giteaWorkspace', () => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const owner = params.get('owner') || '';
|
|
const repo = params.get('repo') || '';
|
|
|
|
return {
|
|
owner, repo,
|
|
showFile: false,
|
|
showEditor: false,
|
|
showPrivate: false,
|
|
privatePages: [],
|
|
editingPrivate: null,
|
|
editingPrivateTitle: '',
|
|
editingPrivateContent: '',
|
|
filePath: '',
|
|
fileContent: '',
|
|
fileSize: 0,
|
|
fileSha: '',
|
|
fileLoading: false,
|
|
fileLanguage: 'text',
|
|
editContent: '',
|
|
commitMessage: 'Update via FlowDeck',
|
|
commitHistory: JSON.parse(localStorage.getItem('fd_commit_msgs') || '[]'),
|
|
// File tree browser
|
|
treeItems: [],
|
|
sortedTreeItems: [],
|
|
treeLoading: false,
|
|
folderStack: [],
|
|
currentPath: '',
|
|
// Context menu
|
|
gwCtx: { visible: false, x: 0, y: 0, item: null },
|
|
|
|
_sortTree() {
|
|
this.sortedTreeItems = [...this.treeItems].sort(function(a, b) {
|
|
if (a.type === b.type) return a.name.localeCompare(b.name);
|
|
return a.type === 'folder' ? -1 : 1;
|
|
});
|
|
},
|
|
|
|
init() {
|
|
// Expose globally for header to access
|
|
window._gwData = this;
|
|
// Set cookie for sidebar
|
|
document.cookie = 'flowdeck_workspace=gitea:' + this.owner + ':' + this.repo + ';path=/;SameSite=Lax';
|
|
// Load sidebar tree (into gitea section)
|
|
this.loadSidebarTree();
|
|
// Load main content tree
|
|
this.loadMainTree('');
|
|
// Listen for sidebar clicks on Gitea items
|
|
this.setupSidebarClicks();
|
|
},
|
|
|
|
async loadMainTree(path) {
|
|
this.treeLoading = true;
|
|
this.currentPath = path;
|
|
try {
|
|
var url = '/api/gitea/projects/' + this.owner + '/' + this.repo + '/tree';
|
|
if (path) url += '?path=' + encodeURIComponent(path);
|
|
var r = await fetch(url);
|
|
if (!r.ok) { this.treeItems = []; this.sortedTreeItems = []; return; }
|
|
var d = await r.json();
|
|
this.treeItems = d.tree || [];
|
|
this._sortTree();
|
|
} catch { this.treeItems = []; this.sortedTreeItems = []; }
|
|
finally { this.treeLoading = false; }
|
|
},
|
|
|
|
drillDown(path) {
|
|
this.folderStack.push(path.split('/').pop());
|
|
this.loadMainTree(path);
|
|
},
|
|
|
|
navigateToFolder(idx) {
|
|
// Truncate stack and rebuild path
|
|
this.folderStack = this.folderStack.slice(0, idx + 1);
|
|
var path = this.folderStack.join('/');
|
|
this.loadMainTree(path);
|
|
},
|
|
|
|
navigateToRoot() {
|
|
this.folderStack = [];
|
|
this.loadMainTree('');
|
|
},
|
|
|
|
openGwContext(ev, item) {
|
|
this.gwCtx = { visible: true, x: ev.clientX, y: ev.clientY, item: item };
|
|
},
|
|
gwRename() {
|
|
this.gwCtx.visible = false;
|
|
var item = this.gwCtx.item;
|
|
if (!item) return;
|
|
var newName = prompt('Rename:', item.name);
|
|
if (!newName || !newName.trim() || newName.trim() === item.name) return;
|
|
var self = this;
|
|
var oldPath = item.path;
|
|
var newPath = oldPath.replace(/[^/]+$/, newName.trim());
|
|
fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/file', {
|
|
method: 'PUT',
|
|
headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ path: oldPath, new_path: newPath, message: 'Rename ' + oldPath + ' to ' + newPath })
|
|
}).then(function(r){ return r.json(); })
|
|
.then(function(d){
|
|
if (d.status === 'ok') { self.refreshTree(); }
|
|
else { window.showToast('Rename failed', 'error'); }
|
|
})
|
|
.catch(function(){ window.showToast('Rename failed', 'error'); });
|
|
},
|
|
gwDelete() {
|
|
this.gwCtx.visible = false;
|
|
var item = this.gwCtx.item;
|
|
if (!item) return;
|
|
if (!confirm('Delete ' + item.name + '?')) return;
|
|
var self = this;
|
|
fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/file?path=' + encodeURIComponent(item.path) + '&sha=' + (item.sha || '') + '&message=Delete ' + item.path, {headers: {'X-CSRF-Token': getCsrf()},
|
|
method: 'DELETE'
|
|
}).then(function(r){
|
|
if (r.ok) { self.refreshTree(); }
|
|
else { window.showToast('Delete failed', 'error'); }
|
|
}).catch(function(){ window.showToast('Delete failed', 'error'); });
|
|
},
|
|
|
|
async loadSidebarTree() {
|
|
try {
|
|
var r = await fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/tree');
|
|
if (!r.ok) {
|
|
// If API fails (e.g. no Gitea token), set a message
|
|
var container = document.getElementById('sidebar-gitea-items');
|
|
if (container) container.innerHTML = '<li class="sidebar-item empty-hint"><span class="page-icon">'+getSvgIcon('link',14)+'</span><span class="page-name text-dim">Connect Gitea to browse files</span></li>';
|
|
return;
|
|
}
|
|
var d = await r.json();
|
|
var items = d.tree || [];
|
|
var container = document.getElementById('sidebar-gitea-items');
|
|
if (!container) return;
|
|
// Ensure gitea section is visible
|
|
if (window.appState && window.appState.sectionsOpen && !window.appState.sectionsOpen.gitea) {
|
|
window.appState.sectionsOpen.gitea = true;
|
|
}
|
|
container.innerHTML = '';
|
|
// Build tree HTML as string and set once (more performant)
|
|
var html = '';
|
|
for (var i = 0; i < items.length; i++) {
|
|
var item = items[i];
|
|
var icon = item.type === 'folder' ? getSvgIcon('folder',14) : getSvgIcon('file',14);
|
|
html += '<li class="sidebar-item" data-gitea-path="' + item.path + '" data-gitea-type="' + (item.type === 'folder' ? 'folder' : 'file') + '" data-gitea-sha="' + (item.sha || '') + '" style="padding-left:12px;cursor:pointer;display:flex;align-items:center;gap:4px;">';
|
|
html += '<span class="page-icon">' + icon + '</span>';
|
|
html += '<span class="page-name">' + item.name + '</span>';
|
|
html += '</li>';
|
|
}
|
|
// Add Private Pages link
|
|
html += '<li style="border-top:1px solid var(--border);margin:8px 0;"></li>';
|
|
html += '<li class="sidebar-item" id="gitea-private-link" style="padding-left:12px;cursor:pointer;display:flex;align-items:center;gap:4px;">';
|
|
html += '<span class="page-icon">'+getSvgIcon('lock',14)+'</span><span class="page-name">Private Pages</span></li>';
|
|
container.innerHTML = html;
|
|
// Re-attach event listeners
|
|
this.setupSidebarClicks();
|
|
} catch(e) {
|
|
console.error('Gitea sidebar tree load failed:', e);
|
|
}
|
|
},
|
|
|
|
setupSidebarClicks() {
|
|
var self = this;
|
|
document.addEventListener('click', function(e) {
|
|
var el = e.target.closest('.sidebar-item[data-gitea-path]');
|
|
if (!el) return;
|
|
|
|
var path = el.getAttribute('data-gitea-path');
|
|
var type = el.getAttribute('data-gitea-type');
|
|
|
|
// If clicking the checkbox, let its own handler deal with selection
|
|
if (e.target.classList.contains('gitea-checkbox')) return;
|
|
|
|
// If clicking the inline rename input, don't navigate
|
|
if (e.target.classList.contains('inline-rename-input')) return;
|
|
|
|
// If Shift or Ctrl/Meta is pressed, use multi-selection
|
|
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
self.selectItem(path, el, e);
|
|
return;
|
|
}
|
|
|
|
// Normal click: navigate (open file/folder)
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Update visual "active" state
|
|
document.querySelectorAll('.sidebar-item.active').forEach(function(si) {
|
|
si.classList.remove('active');
|
|
});
|
|
el.classList.add('active');
|
|
|
|
if (type === 'folder') {
|
|
self.loadSubdir(path, el);
|
|
} else {
|
|
self.openFile(path, el.getAttribute('data-gitea-sha'));
|
|
}
|
|
});
|
|
|
|
// Listen for custom delete event on gitea sidebar items
|
|
document.addEventListener('gitea-delete', function(e) {
|
|
var el = e.target;
|
|
var path = el.getAttribute('data-gitea-path');
|
|
if (!path) return;
|
|
if (!confirm('Delete ' + path + ' from ' + self.owner + '/' + self.repo + '?')) return;
|
|
var sha = el.getAttribute('data-gitea-sha') || '';
|
|
fetch('/api/gitea/projects/' + self.owner + '/' + self.repo + '/file?path=' + encodeURIComponent(path) + '&sha=' + encodeURIComponent(sha) + '&message=' + encodeURIComponent('Delete via FlowDeck'), {headers: {'X-CSRF-Token': getCsrf()},
|
|
method: 'DELETE',
|
|
}).then(function(r) {
|
|
if (r.ok) self.refreshTree();
|
|
}).catch(function() {});
|
|
});
|
|
},
|
|
|
|
async loadSubdir(path, el) {
|
|
var self = this;
|
|
// Check if already loaded
|
|
var ul = el.querySelector('ul');
|
|
if (ul) {
|
|
ul.style.display = ul.style.display === 'none' ? '' : 'none';
|
|
return;
|
|
}
|
|
try {
|
|
var r = await fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/tree?path=' + encodeURIComponent(path));
|
|
if (!r.ok) return;
|
|
var d = await r.json();
|
|
var children = d.tree || [];
|
|
ul = document.createElement('ul');
|
|
ul.className = 'sidebar-items';
|
|
ul.style.paddingLeft = '20px';
|
|
children.forEach(function(child) {
|
|
var icon = child.type === 'folder' ? getSvgIcon('folder',14) : getSvgIcon('file',14);
|
|
var li = document.createElement('li');
|
|
li.className = 'sidebar-item';
|
|
li.setAttribute('data-gitea-path', child.path);
|
|
li.setAttribute('data-gitea-type', child.type === 'folder' ? 'folder' : 'file');
|
|
li.setAttribute('data-gitea-sha', child.sha || '');
|
|
li.style.cssText = 'cursor:pointer;display:flex;align-items:center;gap:4px;';
|
|
// Checkbox
|
|
var cb = document.createElement('input');
|
|
cb.type = 'checkbox';
|
|
cb.className = 'gitea-checkbox';
|
|
cb.style.cssText = 'flex-shrink:0;margin:0;display:none;';
|
|
cb.addEventListener('click', function(ev) {
|
|
ev.stopPropagation();
|
|
self.selectItem(child.path, li, ev);
|
|
});
|
|
li.appendChild(cb);
|
|
// Icon
|
|
var iconSpan = document.createElement('span');
|
|
iconSpan.className = 'sidebar-icon';
|
|
iconSpan.innerHTML = icon; // icon = HTML SVG from getSvgIcon(), NOT text
|
|
li.appendChild(iconSpan);
|
|
// Name
|
|
var nameSpan = document.createElement('span');
|
|
nameSpan.textContent = child.name;
|
|
nameSpan.addEventListener('dblclick', function(ev) {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
self.startInlineRename(nameSpan, child.path, li);
|
|
});
|
|
li.appendChild(nameSpan);
|
|
ul.appendChild(li);
|
|
});
|
|
el.appendChild(ul);
|
|
} catch { /* volontaire */ }
|
|
},
|
|
|
|
async openFile(path, sha) {
|
|
this.filePath = path;
|
|
this.fileSha = sha || '';
|
|
this.showEditor = false;
|
|
this.showFile = true;
|
|
this.fileLoading = true;
|
|
this.fileLanguage = this.getLanguage(path);
|
|
try {
|
|
var r = await fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/file?path=' + encodeURIComponent(path));
|
|
if (r.ok) {
|
|
var d = await r.json();
|
|
this.fileContent = d.content || '';
|
|
this.fileSize = d.content ? d.content.length : 0;
|
|
} else {
|
|
this.fileContent = '[Error loading file]';
|
|
}
|
|
} catch {
|
|
this.fileContent = '[Error loading file]';
|
|
}
|
|
this.fileLoading = false;
|
|
// Highlight after Alpine renders
|
|
var self = this;
|
|
this.$nextTick(function() {
|
|
var block = self.$refs.codeBlock;
|
|
if (block && block.textContent && typeof Prism !== 'undefined') {
|
|
Prism.highlightElement(block);
|
|
}
|
|
});
|
|
},
|
|
|
|
getLanguage(path) {
|
|
var ext = (path || '').split('.').pop().toLowerCase();
|
|
var map = {
|
|
js:'javascript', jsx:'javascript', ts:'typescript', tsx:'typescript',
|
|
py:'python', rb:'ruby', rs:'rust', go:'go', java:'java', kt:'kotlin',
|
|
c:'c', cpp:'c', h:'c', hpp:'c', cs:'csharp',
|
|
html:'markup', htm:'markup', xml:'markup', svg:'markup', css:'css', scss:'css',
|
|
json:'json', yaml:'yaml', yml:'yaml', toml:'toml', ini:'ini',
|
|
md:'markdown', sql:'sql', sh:'bash', bash:'bash', zsh:'bash',
|
|
php:'php', swift:'swift', r:'r', lua:'lua', dart:'dart',
|
|
dockerfile:'docker', makefile:'makefile', cmake:'cmake',
|
|
};
|
|
return map[ext] || 'text';
|
|
},
|
|
|
|
openEditor() {
|
|
this.editContent = this.fileContent;
|
|
this.showEditor = true;
|
|
},
|
|
|
|
async saveFile() {
|
|
if (!this.filePath) return;
|
|
try {
|
|
var r = await fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/file', {
|
|
method: 'PUT',
|
|
headers: {'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken()},
|
|
body: JSON.stringify({
|
|
path: this.filePath, content: this.editContent,
|
|
message: this.commitMessage, sha: this.fileSha,
|
|
})
|
|
});
|
|
if (r.ok) {
|
|
this.fileContent = this.editContent;
|
|
this.showEditor = false;
|
|
if (!this.commitHistory.includes(this.commitMessage)) {
|
|
this.commitHistory.unshift(this.commitMessage);
|
|
if (this.commitHistory.length > 10) this.commitHistory.pop();
|
|
localStorage.setItem('fd_commit_msgs', JSON.stringify(this.commitHistory));
|
|
}
|
|
} else {
|
|
var d = await r.json();
|
|
window.showToast('Failed to save: ' + (d.error || 'Unknown error'), 'error');
|
|
}
|
|
} catch { window.showToast('Network error', 'error'); }
|
|
},
|
|
|
|
async deleteCurrentFile() {
|
|
if (!this.filePath) return;
|
|
if (!confirm('Delete ' + this.filePath + ' from ' + this.owner + '/' + this.repo + '?')) return;
|
|
try {
|
|
var r = await fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/file?path=' + encodeURIComponent(this.filePath) + '&sha=' + encodeURIComponent(this.fileSha) + '&message=' + encodeURIComponent('Delete via FlowDeck'), {headers: {'X-CSRF-Token': getCsrf()},
|
|
method: 'DELETE',
|
|
});
|
|
if (r.ok) {
|
|
this.showFile = false;
|
|
this.filePath = '';
|
|
await this.refreshTree();
|
|
}
|
|
} catch { /* volontaire */ }
|
|
},
|
|
|
|
async refreshTree() {
|
|
// Reload main tree and sidebar tree without full page reload
|
|
this.loadMainTree(this.currentPath);
|
|
this.loadSidebarTree();
|
|
},
|
|
|
|
// ── A20 ph3 : new Date / x-html interdits sous build CSP ──
|
|
closePrivateEdit() {
|
|
this.editingPrivate = null;
|
|
this.editingPrivateTitle = '';
|
|
this.editingPrivateContent = '';
|
|
},
|
|
fmtGwDate(pp) {
|
|
return pp.updated_at ? new Date(pp.updated_at + 'Z').toLocaleString() : '';
|
|
},
|
|
bindGwIcon(el, item) {
|
|
Alpine.effect(() => {
|
|
el.innerHTML = item.type === 'folder' ? getSvgIcon('folder', 16) : getSvgIcon('file', 16);
|
|
});
|
|
},
|
|
formatSize(bytes) {
|
|
if (!bytes) return '';
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1048576) return (bytes/1024).toFixed(1) + ' KB';
|
|
return (bytes/1048576).toFixed(1) + ' MB';
|
|
},
|
|
|
|
// ── Private Pages ──
|
|
|
|
async loadPrivatePages() {
|
|
try {
|
|
var r = await fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/private-pages');
|
|
if (r.ok) { var d = await r.json(); this.privatePages = d.pages || []; }
|
|
} catch { /* volontaire */ }
|
|
},
|
|
|
|
newPrivate() {
|
|
this.editingPrivate = 'new';
|
|
this.editingPrivateTitle = '';
|
|
this.editingPrivateContent = '';
|
|
},
|
|
|
|
async openPrivate(id) {
|
|
try {
|
|
var r = await fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/private-pages/' + id);
|
|
if (r.ok) {
|
|
var d = await r.json();
|
|
this.editingPrivate = id;
|
|
this.editingPrivateTitle = d.page.title;
|
|
this.editingPrivateContent = d.page.content;
|
|
}
|
|
} catch { /* volontaire */ }
|
|
},
|
|
|
|
async savePrivate() {
|
|
var url = '/api/gitea/projects/' + this.owner + '/' + this.repo + '/private-pages';
|
|
var method = 'POST';
|
|
if (this.editingPrivate !== 'new') {
|
|
url += '/' + this.editingPrivate;
|
|
method = 'PUT';
|
|
}
|
|
try {
|
|
var r = await fetch(url, {
|
|
method, headers: {'Content-Type':'application/json','X-CSRF-Token': this.getCsrfToken ? this.getCsrfToken() : ''},
|
|
body: JSON.stringify({title: this.editingPrivateTitle, content: this.editingPrivateContent})
|
|
});
|
|
if (r.ok) {
|
|
this.editingPrivate = null;
|
|
this.editingPrivateTitle = '';
|
|
this.editingPrivateContent = '';
|
|
await this.loadPrivatePages();
|
|
}
|
|
} catch { /* volontaire */ }
|
|
},
|
|
|
|
// Create new file
|
|
showNewFile: false,
|
|
newFilePath: '',
|
|
newFileContent: '',
|
|
newFileMsg: 'Create via FlowDeck',
|
|
async createNewFile() {
|
|
if (!this.newFilePath.trim()) return;
|
|
try {
|
|
var r = await fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/file', {
|
|
method: 'PUT',
|
|
headers: {'Content-Type':'application/json', 'X-CSRF-Token': this.getCsrfToken()},
|
|
body: JSON.stringify({path: this.newFilePath.trim(), content: this.newFileContent, message: this.newFileMsg || 'Create via FlowDeck'})
|
|
});
|
|
if (r.ok) {
|
|
this.showNewFile = false;
|
|
this.newFilePath = '';
|
|
this.newFileContent = '';
|
|
this.newFileMsg = 'Create via FlowDeck';
|
|
await this.loadSidebarTree();
|
|
} else {
|
|
var d = await r.json();
|
|
window.showToast('Failed: ' + (d.error || d.detail || 'Unknown error'), 'error');
|
|
}
|
|
} catch(e) { window.showToast('Error: ' + e.message, 'error'); }
|
|
},
|
|
|
|
// Upload files
|
|
triggerUpload() { document.getElementById('gitea-upload-input').click(); },
|
|
async doUpload(ev) {
|
|
var files = ev.target.files;
|
|
if (!files.length) return;
|
|
for (var i = 0; i < files.length; i++) {
|
|
var form = new FormData();
|
|
form.append('file', files[i]);
|
|
form.append('message', 'Upload ' + files[i].name + ' via FlowDeck');
|
|
try {
|
|
var r = await fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/upload', {
|
|
method: 'POST',
|
|
headers: {'X-CSRF-Token': this.getCsrfToken()},
|
|
body: form
|
|
});
|
|
if (!r.ok) { var d = await r.json(); console.error('Upload failed:', d.error); }
|
|
} catch(e) { console.error('Upload error:', e); }
|
|
}
|
|
await this.loadSidebarTree();
|
|
ev.target.value = '';
|
|
},
|
|
|
|
async deletePrivate(id) {
|
|
if (!confirm('Delete this private page?')) return;
|
|
try {
|
|
var r = await fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/private-pages/' + id, {
|
|
method: 'DELETE', headers: {'X-CSRF-Token': this.getCsrfToken ? this.getCsrfToken() : ''}
|
|
});
|
|
if (r.ok) await this.loadPrivatePages();
|
|
} catch { /* volontaire */ }
|
|
},
|
|
|
|
getCsrfToken() {
|
|
return document.cookie.split('; ').find(function(c) { return c.startsWith('csrf_token='); })?.split('=')[1] || '';
|
|
},
|
|
|
|
// ── Multi-selection ──
|
|
multiSelect: false,
|
|
selectedPaths: [],
|
|
lastClickedPath: null,
|
|
|
|
toggleMultiSelect() {
|
|
this.multiSelect = !this.multiSelect;
|
|
var cbs = document.querySelectorAll('.gitea-checkbox');
|
|
var self = this;
|
|
cbs.forEach(function(cb) { cb.style.display = self.multiSelect ? '' : 'none'; });
|
|
if (!this.multiSelect) {
|
|
// Clear selection when leaving multi-select mode
|
|
cbs.forEach(function(cb) { cb.checked = false; });
|
|
this.selectedPaths = [];
|
|
this.lastClickedPath = null;
|
|
// Remove selected class
|
|
document.querySelectorAll('.sidebar-item.gitea-selected').forEach(function(el) {
|
|
el.classList.remove('gitea-selected');
|
|
});
|
|
}
|
|
},
|
|
|
|
selectItem(path, li, ev) {
|
|
if (ev.shiftKey && this.lastClickedPath) {
|
|
// Range select
|
|
var all = Array.from(document.querySelectorAll('.sidebar-item[data-gitea-path]'));
|
|
var startIdx = all.findIndex(function(el) { return el.getAttribute('data-gitea-path') === this.lastClickedPath; }.bind(this));
|
|
var endIdx = all.findIndex(function(el) { return el.getAttribute('data-gitea-path') === path; }.bind(this));
|
|
if (startIdx >= 0 && endIdx >= 0) {
|
|
var lo = Math.min(startIdx, endIdx);
|
|
var hi = Math.max(startIdx, endIdx);
|
|
this.selectedPaths = [];
|
|
for (var i = lo; i <= hi; i++) {
|
|
this.selectedPaths.push(all[i].getAttribute('data-gitea-path'));
|
|
all[i].classList.add('gitea-selected');
|
|
var cb = all[i].querySelector('.gitea-checkbox');
|
|
if (cb) cb.checked = true;
|
|
}
|
|
}
|
|
this.multiSelect = true;
|
|
this.toggleMultiSelect(); // ensure checkboxes are visible
|
|
} else if (ev.ctrlKey || ev.metaKey) {
|
|
// Toggle single
|
|
var idx = this.selectedPaths.indexOf(path);
|
|
if (idx >= 0) {
|
|
this.selectedPaths.splice(idx, 1);
|
|
li.classList.remove('gitea-selected');
|
|
var cb = li.querySelector('.gitea-checkbox');
|
|
if (cb) cb.checked = false;
|
|
} else {
|
|
this.selectedPaths.push(path);
|
|
li.classList.add('gitea-selected');
|
|
var cb = li.querySelector('.gitea-checkbox');
|
|
if (cb) cb.checked = true;
|
|
}
|
|
this.multiSelect = this.selectedPaths.length > 0;
|
|
this.toggleMultiSelect();
|
|
} else {
|
|
// Normal select — clear multi-selection
|
|
this.selectedPaths = [path];
|
|
this.lastClickedPath = path;
|
|
document.querySelectorAll('.sidebar-item.gitea-selected').forEach(function(el) {
|
|
el.classList.remove('gitea-selected');
|
|
});
|
|
li.classList.add('gitea-selected', 'active');
|
|
var cb = li.querySelector('.gitea-checkbox');
|
|
if (cb) cb.checked = true;
|
|
}
|
|
},
|
|
|
|
isSelected(path) {
|
|
return this.selectedPaths.indexOf(path) >= 0;
|
|
},
|
|
|
|
// ── Inline rename ──
|
|
startInlineRename(nameSpan, path, li) {
|
|
var originalName = nameSpan.textContent;
|
|
var input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.value = originalName;
|
|
input.className = 'inline-rename-input';
|
|
input.style.cssText = 'flex:1;min-width:0;padding:2px 6px;background:var(--bg-tertiary);border:1px solid var(--accent);border-radius:4px;color:var(--text);font-size:13px;outline:none;';
|
|
nameSpan.replaceWith(input);
|
|
input.focus();
|
|
input.select();
|
|
var self = this;
|
|
var finish = function() { self.finishInlineRename(input, originalName, path, li); };
|
|
var cancel = function() {
|
|
var span = document.createElement('span');
|
|
span.className = 'page-name';
|
|
span.textContent = originalName;
|
|
input.replaceWith(span);
|
|
};
|
|
input.addEventListener('blur', finish);
|
|
input.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter') finish();
|
|
if (e.key === 'Escape') { e.stopPropagation(); cancel(); }
|
|
});
|
|
},
|
|
|
|
finishInlineRename(input, originalName, path, li) {
|
|
if (input._renaming) return; // guard against double-fire
|
|
input._renaming = true;
|
|
var newName = input.value.trim();
|
|
if (!newName || newName === originalName) {
|
|
var span = document.createElement('span');
|
|
span.className = 'page-name';
|
|
span.textContent = originalName;
|
|
input.replaceWith(span);
|
|
return;
|
|
}
|
|
var self = this;
|
|
// Construct new path by replacing the last segment
|
|
var parts = path.split('/');
|
|
parts.pop();
|
|
var newPath = (parts.length > 0 ? parts.join('/') + '/' : '') + newName;
|
|
fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/rename', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken()},
|
|
body: JSON.stringify({old_path: path, new_path: newPath, new_name: newName})
|
|
}).then(function(r) {
|
|
if (r.ok) {
|
|
self.refreshTree();
|
|
} else {
|
|
var span = document.createElement('span');
|
|
span.className = 'page-name';
|
|
span.textContent = originalName;
|
|
input.replaceWith(span);
|
|
window.showToast('Rename failed', 'error');
|
|
}
|
|
}).catch(function() {
|
|
var span = document.createElement('span');
|
|
span.className = 'page-name';
|
|
span.textContent = originalName;
|
|
input.replaceWith(span);
|
|
});
|
|
},
|
|
};
|
|
});
|
|
});
|