simplify extension ui
This commit is contained in:
@@ -2,7 +2,7 @@ import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { File } from 'node:buffer';
|
||||
import { handleClipboardAndOpen, singletonPanel } from './shared';
|
||||
import { getToolWebviewStyles, handleClipboardAndOpen, singletonPanel } from './shared';
|
||||
|
||||
const store: { panel: vscode.WebviewPanel | null } = { panel: null };
|
||||
|
||||
@@ -15,6 +15,24 @@ export function openPlecUpload(_context: vscode.ExtensionContext) {
|
||||
try {
|
||||
if (handleClipboardAndOpen(msg)) return;
|
||||
|
||||
if (msg.type === 'pickFolder') {
|
||||
const picked = await vscode.window.showOpenDialog({
|
||||
canSelectFolders: true,
|
||||
canSelectFiles: false,
|
||||
canSelectMany: false,
|
||||
openLabel: 'Select Folder',
|
||||
defaultUri: getPickerDefaultUri(),
|
||||
});
|
||||
if (picked && picked[0]) {
|
||||
const files = await collectHtmlFiles(picked[0].fsPath);
|
||||
panel.webview.postMessage({
|
||||
type: 'filesSelected',
|
||||
files: files.map(filePath => ({ path: filePath, name: path.basename(filePath) })),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'pickFile') {
|
||||
const picked = await vscode.window.showOpenDialog({
|
||||
canSelectMany: true,
|
||||
@@ -159,78 +177,100 @@ function getPickerDefaultUri(): vscode.Uri | undefined {
|
||||
return vscode.Uri.file(root);
|
||||
}
|
||||
|
||||
async function collectHtmlFiles(root: string): Promise<string[]> {
|
||||
const out: string[] = [];
|
||||
const stack: string[] = [root];
|
||||
while (stack.length) {
|
||||
const dir = stack.pop()!;
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const e of entries) {
|
||||
const full = path.join(dir, e.name);
|
||||
if (e.isDirectory()) {
|
||||
if (e.name === 'node_modules' || e.name === '.git') continue;
|
||||
stack.push(full);
|
||||
} else if (e.isFile() && /\.html?$/i.test(e.name)) {
|
||||
out.push(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function getHtml(): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<style>
|
||||
body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); padding: 16px; }
|
||||
h2 { margin-top: 0; }
|
||||
table { width: 100%; border-collapse: collapse; margin-bottom: 12px; }
|
||||
th, td { padding: 6px 8px; text-align: left; vertical-align: middle; border-bottom: 1px solid var(--vscode-panel-border); }
|
||||
th { font-weight: 600; font-size: 12px; text-transform: uppercase; opacity: 0.7; }
|
||||
input[type=text] {
|
||||
width: 100%; padding: 4px 6px;
|
||||
background: var(--vscode-input-background); color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border, transparent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
button {
|
||||
background: var(--vscode-button-background); color: var(--vscode-button-foreground);
|
||||
border: none; padding: 6px 12px; border-radius: 2px; cursor: pointer;
|
||||
}
|
||||
button:hover { background: var(--vscode-button-hoverBackground); }
|
||||
button.secondary {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
button.secondary:hover { background: var(--vscode-button-secondaryHoverBackground); }
|
||||
.file-cell { display: flex; align-items: center; gap: 8px; }
|
||||
.file-name { opacity: 0.85; font-size: 12px; word-break: break-all; }
|
||||
${getToolWebviewStyles()}
|
||||
#rows input[type=text] { width: 100%; }
|
||||
.file-cell { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.row-error { color: var(--vscode-errorForeground); font-size: 12px; margin-top: 4px; }
|
||||
.actions { margin-top: 12px; display: flex; gap: 8px; }
|
||||
#status { margin-top: 12px; min-height: 20px; }
|
||||
.err { color: var(--vscode-errorForeground); white-space: pre-wrap; }
|
||||
.ok { color: var(--vscode-testing-iconPassed, var(--vscode-foreground)); }
|
||||
.result {
|
||||
margin-top: 12px; padding: 10px;
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-left: 3px solid var(--vscode-textBlockQuote-border);
|
||||
word-break: break-all;
|
||||
.pick { min-width: 82px; }
|
||||
.remove-btn {
|
||||
min-width: 28px;
|
||||
width: 28px;
|
||||
padding: 3px 0;
|
||||
color: var(--vscode-errorForeground);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
.result a { color: var(--vscode-textLink-foreground); }
|
||||
.result-actions { margin-top: 8px; display: flex; gap: 8px; }
|
||||
.remove-btn { background: transparent; color: var(--vscode-foreground); padding: 4px 8px; }
|
||||
.pager { margin-top: 8px; display: flex; align-items: center; gap: 8px; justify-content: flex-end; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>PLEC Upload</h2>
|
||||
<p style="opacity:0.8;font-size:12px;margin-top:0;">Pick HTML files, give each an iteration name, then upload. The preview link is returned by the server.</p>
|
||||
<main class="tool-page">
|
||||
<header class="tool-header">
|
||||
<h2 class="tool-title">PLEC Upload</h2>
|
||||
<p class="tool-description">Pick HTML files, give each an iteration name, then upload them to PLEC.</p>
|
||||
</header>
|
||||
|
||||
<table id="rows">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:45%">HTML File</th>
|
||||
<th style="width:45%">Iteration Name</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<div class="actions">
|
||||
<button id="addRow" class="secondary">+ Add Row</button>
|
||||
<button id="upload">Upload</button>
|
||||
</div>
|
||||
|
||||
<div id="status"></div>
|
||||
<div id="results"></div>
|
||||
<section class="tool-panel input-panel">
|
||||
<div class="panel-header">
|
||||
<h3 class="panel-title">Inputs</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="control-group">
|
||||
<div class="file-row">
|
||||
<button id="pickFolder" class="secondary">Select Folder</button>
|
||||
<button id="pickFiles" class="secondary">Select File(s)</button>
|
||||
<button id="clear" class="secondary">Clear</button>
|
||||
</div>
|
||||
<div id="emptySelection" class="file-name" style="margin-top:8px;">(no files selected)</div>
|
||||
<div id="rowsPanel" class="results-panel is-hidden" style="margin-top:8px;">
|
||||
<table id="rows" class="data-table">
|
||||
<thead><tr><th>Filename</th><th style="width:240px;">Iteration Name</th><th style="width:44px;"></th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button id="upload">Upload</button>
|
||||
<button id="openResult" class="secondary" disabled>Open</button>
|
||||
<button id="copyResult" class="secondary" disabled>Copy Link</button>
|
||||
</div>
|
||||
<div id="status" class="status-panel"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
const PAGE_SIZE = 5;
|
||||
let nextId = 1;
|
||||
let rows = [];
|
||||
let page = 0;
|
||||
let previewUrl = '';
|
||||
const tbody = document.querySelector('#rows tbody');
|
||||
const rowsPanel = document.getElementById('rowsPanel');
|
||||
const emptySelection = document.getElementById('emptySelection');
|
||||
|
||||
function parseIterationName(filename) {
|
||||
const base = filename.replace(/\\.html?$/i, '');
|
||||
@@ -241,87 +281,143 @@ function getHtml(): string {
|
||||
return base;
|
||||
}
|
||||
const statusEl = document.getElementById('status');
|
||||
const resultsEl = document.getElementById('results');
|
||||
const openResultBtn = document.getElementById('openResult');
|
||||
const copyResultBtn = document.getElementById('copyResult');
|
||||
|
||||
function addRow() {
|
||||
const id = 'r' + (nextId++);
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.id = id;
|
||||
tr.innerHTML = \`
|
||||
<td>
|
||||
<div class="file-cell">
|
||||
<button class="pick secondary">Choose...</button>
|
||||
<span class="file-name" data-name>(no file)</span>
|
||||
</div>
|
||||
<input type="hidden" data-path />
|
||||
<div class="row-error" data-error></div>
|
||||
</td>
|
||||
<td><input type="text" data-iter placeholder="iteration name" /></td>
|
||||
<td><button class="remove-btn" title="Remove">×</button></td>
|
||||
\`;
|
||||
tr.querySelector('.pick').addEventListener('click', () => {
|
||||
vscode.postMessage({ type: 'pickFile', rowId: id });
|
||||
});
|
||||
tr.querySelector('.remove-btn').addEventListener('click', () => {
|
||||
tr.remove();
|
||||
if (!tbody.children.length) addRow();
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
|
||||
document.getElementById('addRow').addEventListener('click', addRow);
|
||||
function addFiles(files) {
|
||||
const existing = new Set(rows.map(r => r.path));
|
||||
files.forEach(f => {
|
||||
if (existing.has(f.path)) return;
|
||||
rows.push({
|
||||
id: 'r' + (nextId++),
|
||||
path: f.path,
|
||||
name: f.name,
|
||||
iteration: parseIterationName(f.name),
|
||||
error: ''
|
||||
});
|
||||
existing.add(f.path);
|
||||
});
|
||||
normalizeDuplicateIterations(files.length);
|
||||
page = Math.max(0, Math.ceil(rows.length / PAGE_SIZE) - 1);
|
||||
renderRows();
|
||||
}
|
||||
|
||||
function normalizeDuplicateIterations(addedCount) {
|
||||
if (addedCount <= 1) return;
|
||||
const added = rows.slice(Math.max(0, rows.length - addedCount));
|
||||
if (!added.length) return;
|
||||
const first = added[0].iteration;
|
||||
if (first && added.every(r => r.iteration === first)) {
|
||||
added.forEach((r, i) => {
|
||||
r.iteration = String(i + 1).padStart(2, '0') + '_' + first;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderRows() {
|
||||
const oldPager = document.getElementById('selectedPager');
|
||||
if (oldPager) oldPager.remove();
|
||||
const maxPage = Math.max(0, Math.ceil(rows.length / PAGE_SIZE) - 1);
|
||||
page = Math.min(page, maxPage);
|
||||
rowsPanel.classList.toggle('is-hidden', rows.length === 0);
|
||||
emptySelection.classList.toggle('is-hidden', rows.length !== 0);
|
||||
if (!rows.length) {
|
||||
tbody.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
const start = page * PAGE_SIZE;
|
||||
const pageRows = rows.slice(start, start + PAGE_SIZE);
|
||||
tbody.innerHTML = pageRows.map((row, offset) => {
|
||||
const i = start + offset;
|
||||
return \`
|
||||
<tr data-id="\${row.id}">
|
||||
<td>
|
||||
<span class="mono wrap">\${escapeHtml(row.name)}</span>
|
||||
<div class="row-error" data-error>\${escapeHtml(row.error || '')}</div>
|
||||
</td>
|
||||
<td><input type="text" data-iter data-index="\${i}" value="\${escapeHtml(row.iteration)}" /></td>
|
||||
<td><button class="remove-btn danger" data-remove="\${i}" title="Remove">×</button></td>
|
||||
</tr>
|
||||
\`;
|
||||
}).join('');
|
||||
tbody.querySelectorAll('[data-iter]').forEach(input => {
|
||||
input.addEventListener('input', () => {
|
||||
rows[Number(input.dataset.index)].iteration = input.value;
|
||||
});
|
||||
});
|
||||
tbody.querySelectorAll('[data-remove]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
rows.splice(Number(btn.dataset.remove), 1);
|
||||
renderRows();
|
||||
});
|
||||
});
|
||||
if (rows.length > PAGE_SIZE) {
|
||||
const pager = document.createElement('div');
|
||||
pager.id = 'selectedPager';
|
||||
pager.className = 'pager';
|
||||
pager.innerHTML =
|
||||
'<button class="secondary" id="prevPage"' + (page === 0 ? ' disabled' : '') + '>Previous</button>' +
|
||||
'<span class="file-name">Page ' + (page + 1) + ' of ' + (maxPage + 1) + '</span>' +
|
||||
'<button class="secondary" id="nextPage"' + (page === maxPage ? ' disabled' : '') + '>Next</button>';
|
||||
document.querySelector('#rows').parentElement.after(pager);
|
||||
pager.querySelector('#prevPage').addEventListener('click', () => { page--; renderRows(); });
|
||||
pager.querySelector('#nextPage').addEventListener('click', () => { page++; renderRows(); });
|
||||
}
|
||||
}
|
||||
|
||||
function setPreview(url) {
|
||||
previewUrl = url || '';
|
||||
openResultBtn.disabled = !previewUrl;
|
||||
copyResultBtn.disabled = !previewUrl;
|
||||
}
|
||||
|
||||
document.getElementById('pickFolder').addEventListener('click', () => {
|
||||
vscode.postMessage({ type: 'pickFolder' });
|
||||
});
|
||||
document.getElementById('pickFiles').addEventListener('click', () => {
|
||||
vscode.postMessage({ type: 'pickFile' });
|
||||
});
|
||||
document.getElementById('clear').addEventListener('click', () => {
|
||||
rows = [];
|
||||
page = 0;
|
||||
setPreview('');
|
||||
renderRows();
|
||||
statusEl.textContent = '';
|
||||
});
|
||||
|
||||
document.getElementById('upload').addEventListener('click', () => {
|
||||
clearErrors();
|
||||
statusEl.textContent = '';
|
||||
resultsEl.innerHTML = '';
|
||||
const rows = [...tbody.querySelectorAll('tr')].map(tr => ({
|
||||
id: tr.dataset.id,
|
||||
path: tr.querySelector('[data-path]').value,
|
||||
iteration: tr.querySelector('[data-iter]').value,
|
||||
})).filter(r => r.path || r.iteration);
|
||||
setPreview('');
|
||||
if (rows.length === 0) {
|
||||
statusEl.innerHTML = '<span class="err">Add at least one row with a file and iteration name.</span>';
|
||||
statusEl.innerHTML = '<span class="err">Select at least one file.</span>';
|
||||
return;
|
||||
}
|
||||
vscode.postMessage({ type: 'upload', rows });
|
||||
});
|
||||
openResultBtn.addEventListener('click', () => {
|
||||
if (previewUrl) vscode.postMessage({ type: 'open', text: previewUrl });
|
||||
});
|
||||
copyResultBtn.addEventListener('click', () => {
|
||||
if (previewUrl) vscode.postMessage({ type: 'copy', text: previewUrl });
|
||||
});
|
||||
|
||||
function clearErrors() {
|
||||
document.querySelectorAll('[data-error]').forEach(el => el.textContent = '');
|
||||
rows.forEach(row => row.error = '');
|
||||
renderRows();
|
||||
}
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
const m = e.data;
|
||||
if (m.type === 'filesSelected') {
|
||||
const firstTr = tbody.querySelector('tr[data-id="' + m.rowId + '"]');
|
||||
const affected = [];
|
||||
m.files.forEach(function(f, i) {
|
||||
let tr;
|
||||
if (i === 0) {
|
||||
tr = firstTr;
|
||||
} else {
|
||||
addRow();
|
||||
tr = tbody.lastElementChild;
|
||||
}
|
||||
if (!tr) return;
|
||||
tr.querySelector('[data-path]').value = f.path;
|
||||
tr.querySelector('[data-name]').textContent = f.name;
|
||||
const iter = tr.querySelector('[data-iter]');
|
||||
if (!iter.value) iter.value = parseIterationName(f.name);
|
||||
affected.push(tr);
|
||||
});
|
||||
if (affected.length > 1) {
|
||||
const iters = affected.map(r => r.querySelector('[data-iter]').value);
|
||||
if (iters[0] !== '' && iters.every(n => n === iters[0])) {
|
||||
affected.forEach((r, i) => {
|
||||
r.querySelector('[data-iter]').value = String(i + 1).padStart(2, '0') + '_' + iters[0];
|
||||
});
|
||||
}
|
||||
}
|
||||
addFiles(m.files || []);
|
||||
} else if (m.type === 'status') {
|
||||
statusEl.textContent = m.message;
|
||||
statusEl.className = '';
|
||||
statusEl.className = 'status-panel';
|
||||
} else if (m.type === 'error') {
|
||||
statusEl.innerHTML = '';
|
||||
const div = document.createElement('div');
|
||||
@@ -329,47 +425,17 @@ function getHtml(): string {
|
||||
div.textContent = m.message;
|
||||
statusEl.appendChild(div);
|
||||
} else if (m.type === 'rowError') {
|
||||
const tr = tbody.querySelector('tr[data-id="' + m.rowId + '"]');
|
||||
if (tr) tr.querySelector('[data-error]').textContent = m.message;
|
||||
const row = rows.find(r => r.id === m.rowId);
|
||||
if (row) row.error = m.message;
|
||||
renderRows();
|
||||
statusEl.innerHTML = '<span class="err">Fix row errors and retry.</span>';
|
||||
} else if (m.type === 'result') {
|
||||
statusEl.innerHTML = '<span class="ok">Upload complete.</span>';
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'result';
|
||||
if (m.preview) {
|
||||
wrap.innerHTML = '<div><strong>Preview:</strong> <a href="' + m.preview + '">' + m.preview + '</a></div>';
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'result-actions';
|
||||
const copy = document.createElement('button');
|
||||
copy.textContent = 'Copy';
|
||||
copy.onclick = () => vscode.postMessage({ type: 'copy', text: m.preview });
|
||||
const open = document.createElement('button');
|
||||
open.textContent = 'Open';
|
||||
open.className = 'secondary';
|
||||
open.onclick = () => vscode.postMessage({ type: 'open', text: m.preview });
|
||||
const showRaw = document.createElement('button');
|
||||
showRaw.textContent = 'Show server response';
|
||||
showRaw.className = 'secondary';
|
||||
showRaw.onclick = () => {
|
||||
const pre = document.createElement('pre');
|
||||
pre.style.marginTop = '8px';
|
||||
pre.style.whiteSpace = 'pre-wrap';
|
||||
pre.textContent = m.rawText || JSON.stringify(m.raw, null, 2);
|
||||
wrap.appendChild(pre);
|
||||
showRaw.disabled = true;
|
||||
};
|
||||
actions.appendChild(copy); actions.appendChild(open); actions.appendChild(showRaw);
|
||||
wrap.appendChild(actions);
|
||||
} else {
|
||||
wrap.innerHTML = '<div>No preview field in response. Raw JSON:</div><pre>' +
|
||||
(m.rawText || JSON.stringify(m.raw, null, 2)).replace(/</g, '<') + '</pre>';
|
||||
}
|
||||
resultsEl.innerHTML = '';
|
||||
resultsEl.appendChild(wrap);
|
||||
setPreview(m.preview);
|
||||
}
|
||||
});
|
||||
|
||||
addRow();
|
||||
renderRows();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
Reference in New Issue
Block a user