refactor: extraction des modules xlsx, lien dashboard-grille et inspecteur redimensionnable #154
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
/* ObsiGate — XLSX command bar builder (#154-A5).
|
||||
*
|
||||
* Pure string builder for the spreadsheet shell (sheet tabs + grouped action
|
||||
* buttons + status pills). Extracted from frontend/js/viewer.js so the markup
|
||||
* is testable and the viewer only wires behaviour.
|
||||
*/
|
||||
import { t } from '../i18n.js';
|
||||
import { escapeHtml } from '../utils.js';
|
||||
|
||||
/**
|
||||
* @param {object} ctx
|
||||
* @param {Array<{name: string}>} ctx.sheets
|
||||
* @param {boolean} ctx.isCsv
|
||||
* @param {boolean} ctx.readOnly
|
||||
* @param {boolean} ctx.editable - not a CSV and not read-only
|
||||
* @returns {{ tabs: string, actionsHtml: string, statusBar: string }}
|
||||
*/
|
||||
export function buildCommandBar({ sheets, isCsv, readOnly, editable }) {
|
||||
// Sheet tabs are always rendered (even for a single sheet) so the strip reads
|
||||
// as a real affordance; an editable workbook gets an explicit “+” button.
|
||||
const tabs = isCsv
|
||||
? ""
|
||||
: `<div class="xlsx-tabs" role="tablist">${sheets.map((s, i) =>
|
||||
`<button class="xlsx-tab${i === 0 ? " active" : ""}" data-sheet="${i}" role="tab" aria-selected="${i === 0}">${escapeHtml(s.name)}</button>`
|
||||
).join("")}${editable
|
||||
? `<button class="xlsx-tab-add" id="xlsx-tab-add" type="button" title="${escapeHtml(t("xlsx.tabs_add_sheet"))}" aria-label="${escapeHtml(t("xlsx.tabs_add_sheet"))}">+</button>`
|
||||
: ""}</div>`;
|
||||
|
||||
// Status pills make the viewer's limits visible up front.
|
||||
const statusPills = [
|
||||
readOnly
|
||||
? `<span class="xlsx-status-pill xlsx-status-readonly" title="${escapeHtml(t("xlsx.readonly_hint"))}"><i data-lucide="lock" class="xlsx-status-icon"></i>${escapeHtml(t("xlsx.readonly_badge"))}</span>`
|
||||
: "",
|
||||
!isCsv
|
||||
? `<span class="xlsx-status-pill xlsx-status-formula" title="${escapeHtml(t("xlsx.formulas_note_title"))}"><i data-lucide="sigma" class="xlsx-status-icon"></i>${escapeHtml(t("xlsx.formulas_note"))}</span>`
|
||||
: "",
|
||||
].filter(Boolean).join("");
|
||||
const statusBar = statusPills ? `<div class="xlsx-status-bar">${statusPills}</div>` : "";
|
||||
|
||||
// Command groups, separated by thin rules.
|
||||
const actionGroups = [];
|
||||
if (!readOnly) {
|
||||
actionGroups.push(`<span class="xlsx-cmd-group" data-group="history">
|
||||
<button class="btn-action" id="xlsx-undo-btn" type="button" title="${escapeHtml(t("xlsx.undo"))}" disabled>
|
||||
<i data-lucide="undo-2" style="width:14px;height:14px"></i>
|
||||
</button>
|
||||
<button class="btn-action" id="xlsx-redo-btn" type="button" title="${escapeHtml(t("xlsx.redo"))}" disabled>
|
||||
<i data-lucide="redo-2" style="width:14px;height:14px"></i>
|
||||
</button>
|
||||
</span>`);
|
||||
actionGroups.push(`<span class="xlsx-cmd-group" data-group="formulas">
|
||||
<button class="btn-action xlsx-formula-toggle" id="xlsx-formula-btn" type="button"
|
||||
aria-pressed="false" title="${escapeHtml(t("xlsx.formula_toggle_title"))}">f(x)</button>
|
||||
</span>`);
|
||||
}
|
||||
if (editable) {
|
||||
actionGroups.push(`<span class="xlsx-cmd-group" data-group="insert">
|
||||
<button class="btn-action" id="xlsx-structure-btn" title="${escapeHtml(t("xlsx.structure_btn"))}">
|
||||
<i data-lucide="table-properties" style="width:14px;height:14px"></i>
|
||||
</button>
|
||||
</span>`);
|
||||
actionGroups.push(`<span class="xlsx-cmd-group" id="xlsx-view-group" data-group="view"></span>`);
|
||||
}
|
||||
actionGroups.push(`<span class="xlsx-cmd-group" data-group="file">
|
||||
<button class="btn-action" id="xlsx-download-btn">
|
||||
<i data-lucide="download" style="width:14px;height:14px"></i> ${t("viewer.download")}
|
||||
</button>
|
||||
<button class="btn-action" id="xlsx-csv-btn" title="${escapeHtml(t("xlsx.csv_export"))}">
|
||||
<i data-lucide="file-spreadsheet" style="width:14px;height:14px"></i> CSV
|
||||
</button>
|
||||
<button class="btn-action xlsx-save-primary" id="xlsx-save-btn" disabled>${t("common.save")}</button>
|
||||
</span>`);
|
||||
const actionsHtml = actionGroups.join('<span class="xlsx-cmd-sep" aria-hidden="true"></span>');
|
||||
|
||||
return { tabs, actionsHtml, statusBar };
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/* ObsiGate — XLSX dashboard renderer (#154-A5).
|
||||
*
|
||||
* Pure string builders for the workbook dashboard inspector (named ranges, KPI
|
||||
* cards). Extracted from frontend/js/viewer.js; the viewer handles the fetch,
|
||||
* the inspector host and the range → grid link.
|
||||
*/
|
||||
import { t } from '../i18n.js';
|
||||
import { escapeHtml } from '../utils.js';
|
||||
|
||||
export function renderDashboardLoading() {
|
||||
return `<div class="xlsx-dashboard-loading">…</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} dash - payload of GET …/xlsx/dashboard
|
||||
* @returns {string} the inspector body markup
|
||||
*/
|
||||
export function renderDashboardHtml(dash) {
|
||||
const namedRanges = dash.named_ranges || [];
|
||||
const sheets = dash.sheets || [];
|
||||
const rangeRows = namedRanges.map((r) =>
|
||||
`<tr class="xlsx-range-row" data-ref="${escapeHtml(r.ref || "")}"><td><code>${escapeHtml(r.name)}</code></td><td>${escapeHtml(r.scope || "—")}</td><td><code>${escapeHtml(r.ref)}</code></td></tr>`,
|
||||
).join("");
|
||||
const kpiCards = sheets.map((s) => {
|
||||
const cards = (s.kpi || []).map((k) =>
|
||||
`<span class="xlsx-kpi"><span class="xlsx-kpi-label">${escapeHtml(k.label)}</span><span class="xlsx-kpi-value">${escapeHtml(String(k.value))}</span></span>`,
|
||||
).join("");
|
||||
return `<div class="xlsx-kpi-sheet">
|
||||
<h4>${escapeHtml(s.name)}</h4>
|
||||
<p class="xlsx-kpi-meta">${escapeHtml(t("xlsx.dashboard_stats", {
|
||||
cells: s.cells, rows: s.rows, cols: s.cols, formulas: s.formulas, numeric: s.numeric,
|
||||
}))}</p>
|
||||
<div class="xlsx-kpi-cards">${cards || "<span class=\"xlsx-kpi-empty\">—</span>"}</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
return `
|
||||
<div class="xlsx-dashboard-head">
|
||||
<h3><i data-lucide="layout-dashboard" style="width:14px;height:14px"></i> ${escapeHtml(t("xlsx.dashboard_title"))}</h3>
|
||||
<span class="xlsx-dashboard-objects">
|
||||
${escapeHtml(t("xlsx.dashboard_charts", { n: dash.objects?.charts ?? 0 }))}
|
||||
· ${escapeHtml(t("xlsx.dashboard_pivots", { n: dash.objects?.pivots ?? 0 }))}
|
||||
</span>
|
||||
</div>
|
||||
${namedRanges.length || sheets.length ? "" : `<p class="xlsx-kpi-empty">${escapeHtml(t("xlsx.dashboard_empty"))}</p>`}
|
||||
${namedRanges.length ? `
|
||||
<table class="csv-table xlsx-ranges-table"><thead><tr>
|
||||
<th>${escapeHtml(t("xlsx.dashboard_nr_name"))}</th>
|
||||
<th>${escapeHtml(t("xlsx.dashboard_nr_scope"))}</th>
|
||||
<th>${escapeHtml(t("xlsx.dashboard_nr_ref"))}</th>
|
||||
</tr></thead><tbody>${rangeRows}</tbody></table>` : ""}
|
||||
<div class="xlsx-kpi-grid">${kpiCards}</div>
|
||||
<p class="xlsx-dashboard-hint">${escapeHtml(t("xlsx.dashboard_hint"))}</p>`;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/* ObsiGate — XLSX A1 reference helpers (#154-A5).
|
||||
*
|
||||
* Pure functions (no DOM, no i18n): safe to unit-test in Node and shared by the
|
||||
* viewer. Extracted from frontend/js/viewer.js without behaviour change.
|
||||
*/
|
||||
|
||||
/** Parse an A1 reference ("B12") into its 1-based (row, col) parts. */
|
||||
export function parseRef(ref) {
|
||||
const m = /^([A-Z]+)(\d+)$/.exec(ref || "");
|
||||
if (!m) return null;
|
||||
let col = 0;
|
||||
for (const ch of m[1]) col = col * 26 + (ch.charCodeAt(0) - 64);
|
||||
return { row: Number(m[2]), col };
|
||||
}
|
||||
|
||||
/** Column number (1-based) → letters ("A", "Z", "AA"). */
|
||||
export function columnName(col) {
|
||||
let name = "";
|
||||
while (col > 0) {
|
||||
const rem = (col - 1) % 26;
|
||||
name = String.fromCharCode(65 + rem) + name;
|
||||
col = Math.floor((col - 1) / 26);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Find a rendered cell inside a sheet panel by (row, col). */
|
||||
export function findTd(panel, row, col) {
|
||||
return panel.querySelector(`td[data-cell="${columnName(col)}${row}"]`);
|
||||
}
|
||||
|
||||
/** Sheet prefix of a range ref ("'Mon onglet'!$A$1" → "Mon onglet"), or null. */
|
||||
export function sheetOfRef(ref) {
|
||||
const s = String(ref || "");
|
||||
if (!s.includes("!")) return null;
|
||||
return s.split("!")[0].replace(/^'|'$/g, "");
|
||||
}
|
||||
|
||||
/** First cell of a range ref ("Data!$A$1:$B$5" → "A1"), or null. */
|
||||
export function firstCellOfRange(ref) {
|
||||
const s = String(ref || "");
|
||||
const body = s.includes("!") ? s.split("!").pop() : s;
|
||||
const m = /([A-Za-z]+)\$?(\d+)/.exec(body);
|
||||
return m ? `${m[1].toUpperCase()}${m[2]}` : null;
|
||||
}
|
||||
Reference in New Issue
Block a user