BUILD142 — новый вид ручного SQL Mapping

This commit is contained in:
2026-09-15 17:00:28 +03:00
parent 4d067c3e9c
commit fdc5a3f83c
3 changed files with 170 additions and 26 deletions

2
app.py
View File

@@ -29,7 +29,7 @@ from ui_builder import install_ui_builder
from khl_site.khl_data_center import APP as khl_site_app
BASE_DIR = Path(__file__).resolve().parent
BUILD_VERSION = "2026.09.15.9"
BUILD_VERSION = "2026.09.15.10"
# compatibility: BUILD_VERSION = "2026.08.26.1"
# compatibility: BUILD_VERSION = "2026.08.25.1"
# compatibility: BUILD_VERSION = "2026.08.24.15"

View File

@@ -2474,3 +2474,121 @@ body.hockey-admin-settings-open { overflow: hidden; }
.hockey-map-table-column-nav>small{font-size:0}
.hockey-map-table-column-nav>small::after{content:"⇆";font-size:11px}
}
/* BUILD142: row + field browser for manual SQL Mapping. No horizontal spreadsheet scrolling. */
.hockey-map-table-picker-v2{max-height:56%!important;min-height:190px!important}
.hockey-map-table-browser{
height:min(310px,31dvh);
min-height:160px;
display:grid;
grid-template-columns:minmax(118px,.42fr) minmax(0,1.58fr);
gap:8px;
overflow:hidden;
}
.hockey-map-table-rows,.hockey-map-table-fields{
min-height:0;
border:1px solid #263d51;
border-radius:9px;
background:#0a131c;
overflow:hidden;
}
.hockey-map-table-rows{display:grid;grid-template-rows:auto minmax(0,1fr)}
.hockey-map-table-rows>header,.hockey-map-table-fields>header{
min-height:31px;
box-sizing:border-box;
display:flex;
align-items:center;
justify-content:space-between;
gap:8px;
padding:6px 8px;
border-bottom:1px solid #263d51;
background:#132235;
}
.hockey-map-table-rows>header span,.hockey-map-table-fields>header span{
color:#69c9ef;
font-size:8px;
font-weight:950;
letter-spacing:.09em;
}
.hockey-map-table-rows>header b,.hockey-map-table-fields>header>b{
flex:0 0 auto;
min-width:22px;
padding:3px 5px;
border-radius:999px;
background:#173148;
color:#8ed5ef;
font-size:8px;
text-align:center;
}
.hockey-map-table-rows>div{
min-height:0;
overflow-y:auto;
overflow-x:hidden;
scrollbar-width:thin;
scrollbar-color:#3d6486 #09131d;
}
.hockey-map-table-row{
width:100%;
min-width:0;
display:grid;
grid-template-columns:30px minmax(0,1fr);
gap:1px 6px;
padding:7px 8px;
border:0;
border-bottom:1px solid #213448;
background:#0f1a26;
color:#dce7f2;
text-align:left;
cursor:pointer;
}
.hockey-map-table-row:hover{background:#132638}
.hockey-map-table-row.is-active{background:#15322f;box-shadow:inset 3px 0 0 #48dfbd}
.hockey-map-table-row>b{grid-row:1/3;color:#69c9ef;font-size:10px;align-self:center}
.hockey-map-table-row>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#dce7f2;font-size:9px;font-weight:800}
.hockey-map-table-row>small{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#7189a0;font-size:8px}
.hockey-map-table-fields{display:grid;grid-template-rows:auto minmax(0,1fr) auto}
.hockey-map-table-fields>header>div{min-width:0;display:grid;gap:2px}
.hockey-map-table-fields>header strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#dceaf6;font-size:10px}
.hockey-map-table-fields-list{
min-height:0;
overflow-y:auto;
overflow-x:hidden;
display:grid;
grid-template-columns:repeat(2,minmax(0,1fr));
align-content:start;
gap:6px;
padding:7px;
scrollbar-width:thin;
scrollbar-color:#3d6486 #09131d;
}
.hockey-map-table-field{
min-width:0;
display:grid;
gap:4px;
padding:7px 8px;
border:1px solid #2d455d;
border-radius:8px;
background:#101c29;
color:#dce7f2;
text-align:left;
cursor:pointer;
}
.hockey-map-table-field:hover{border-color:#4f7898;background:#132536}
.hockey-map-table-field.is-selected{border-color:#48dfbd;background:#15322f;box-shadow:inset 0 0 0 1px rgba(72,223,189,.18)}
.hockey-map-table-field:disabled{opacity:.34;cursor:not-allowed}
.hockey-map-table-field-head{min-width:0;display:flex;align-items:center;gap:5px}
.hockey-map-table-field-head strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#9fc4df;font-size:9px;text-transform:none}
.hockey-map-table-field em{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#f2f7fb;font-size:11px;font-style:normal;font-weight:800}
.hockey-map-table-field code{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#68839b;font:8px Consolas,monospace}
.hockey-map-table-fields-hint{padding:6px 8px;border-top:1px solid #22364a;color:#71889e;font-size:8px}
.hockey-map-table-no-rows{padding:12px;color:#71889e;font-size:9px;text-align:center}
@media(max-width:1180px){
.hockey-map-table-browser{grid-template-columns:105px minmax(0,1fr)}
.hockey-map-table-fields-list{grid-template-columns:1fr}
}
@media(max-width:760px){
.hockey-map-table-picker-v2{max-height:62%!important}
.hockey-map-table-browser{grid-template-columns:1fr;grid-template-rows:minmax(90px,.55fr) minmax(130px,1.45fr)}
.hockey-map-table-fields-list{grid-template-columns:repeat(2,minmax(0,1fr))}
}

View File

@@ -2372,37 +2372,56 @@
});
const visibleRows = filteredRows.slice(0, 120);
const columns = manualColumns;
// BUILD141: keep the horizontal scrollbar inside the visible Mapping viewport.
// A tall SQL table used to extend below the clipped data browser, hiding the
// horizontal scrollbar and making right-side columns unreachable.
const tableGridHeight = Math.min(360, Math.max(210, visibleRows.length * 44 + 42));
const cells = visibleRows.map((row) => `
<tr>
<th><b>${Number(row.index || 0)}</b><span>${escapeHtml(row.label || `Строка ${row.index}`)}</span></th>
${columns.map((column) => {
const key = `${selectedTable.code}.row.${row.index}.${column.key}`;
const source = mappingSourceByKey(key);
const compatible = source && mappingCompatible(source, targetKind);
return `<td><button type="button" data-map-source="${escapeHtml(key)}" class="${selectedSource === key ? "is-selected" : ""}" ${compatible ? "" : "disabled"} title="${escapeHtml(key)}"><strong>${escapeHtml(mappingDisplayValue(row.values?.[column.key], column.kind || "text"))}</strong><small>${escapeHtml(column.label || column.key)}</small></button></td>`;
}).join("")}
</tr>`).join("");
// BUILD142: manual SQL linking no longer uses a wide spreadsheet.
// The operator chooses a row in a compact list and then sees every SQL
// column of that row as a vertical/card field browser. This keeps all
// columns reachable on narrow Mapping panels without horizontal scrolling.
let browserRow = visibleRows.find((row) => Number(row.index || 0) === Number(state.mappingSelectedTableRow || 0)) || visibleRows[0] || selectedTableRow || null;
if (browserRow) state.mappingSelectedTableRow = Number(browserRow.index || 1);
const rowButtons = visibleRows.map((row) => {
const preview = columns.slice(0, 4)
.map((column) => mappingDisplayValue(row.values?.[column.key], column.kind || "text"))
.map((value) => String(value || "").trim())
.filter(Boolean)
.slice(0, 2)
.join(" · ");
const active = Number(row.index || 0) === Number(browserRow?.index || 0);
return `<button type="button" class="hockey-map-table-row ${active ? "is-active" : ""}" data-map-table-row-pick="${Number(row.index || 0)}"><b>#${Number(row.index || 0)}</b><span>${escapeHtml(row.label || `Строка ${row.index}`)}</span><small>${escapeHtml(preview || "Пустая строка")}</small></button>`;
}).join("");
const fieldCards = browserRow ? columns.map((column) => {
const key = `${selectedTable.code}.row.${browserRow.index}.${column.key}`;
const source = mappingSourceByKey(key);
const compatible = source && (!targetKind || mappingCompatible(source, targetKind));
const value = source ? source.value : browserRow.values?.[column.key];
const selected = selectedSource === key;
return `<button type="button" class="hockey-map-table-field ${selected ? "is-selected" : ""}" data-map-source="${escapeHtml(key)}" ${compatible ? "" : "disabled"} title="${escapeHtml(key)}">
<span class="hockey-map-table-field-head"><strong>${escapeHtml(column.label || column.key)}</strong>${column.localized ? `<i class="hockey-map-lang-auto">AUTO ${String(column.language || selectedTable?.language || "ru").toUpperCase()}</i>` : ""}</span>
<em>${escapeHtml(mappingDisplayValue(value, source?.kind || column.kind || "text"))}</em>
<code>${escapeHtml(column.key)}</code>
</button>`;
}).join("") : "";
tableMarkup = `
<details class="hockey-map-table-picker" data-map-sql-table-details ${state.mappingSqlTableOpen ? "open" : ""}>
<summary><div><span>ТАБЛИЦА SQL</span><strong>Выберите конкретную строку и столбец</strong></div><b>${Number(selectedTable?.row_count || 0)} строк · ${columns.length} колонок</b></summary>
<details class="hockey-map-table-picker hockey-map-table-picker-v2" data-map-sql-table-details ${state.mappingSqlTableOpen ? "open" : ""}>
<summary><div><span>ТАБЛИЦА SQL</span><strong>Строка → поле</strong></div><b>${Number(selectedTable?.row_count || 0)} строк · ${columns.length} колонок</b></summary>
<div class="hockey-map-table-controls">
<select data-map-table-source>${tables.map((table) => `<option value="${escapeHtml(table.code)}" ${String(table.code) === String(selectedTable?.code) ? "selected" : ""}>${escapeHtml(table.name || table.code)} · ${Number(table.row_count || 0)} строк</option>`).join("")}</select>
<label><span>⌕</span><input data-map-table-row-search value="${escapeHtml(state.mappingTableRowSearch)}" placeholder="Поиск строки по любому значению…"></label>
</div>
<div class="hockey-map-table-column-nav">
<span>КОЛОНКИ</span>
<button type="button" data-map-table-scroll="start" title="В начало таблицы">⇤</button>
<button type="button" data-map-table-scroll="left" title="Прокрутить колонки влево">←</button>
<small>горизонтальная прокрутка</small>
<button type="button" data-map-table-scroll="right" title="Прокрутить колонки вправо">→</button>
<button type="button" data-map-table-scroll="end" title="В конец таблицы">⇥</button>
</div>
${selectedTable?.skipped ? `<div class="hockey-directory-empty">Не хватает параметров: ${escapeHtml((selectedTable.missing || []).map((x) => `:${x}`).join(", "))}</div>` : selectedTable?.error ? `<div class="hockey-directory-empty is-error">${escapeHtml(selectedTable.error)}</div>` : `
<div class="hockey-map-table-grid" style="height:min(${tableGridHeight}px,34dvh)"><table><thead><tr><th>Строка</th>${columns.map((column) => `<th><strong>${escapeHtml(column.label || column.key)}${column.localized ? ` <i class="hockey-map-lang-auto">AUTO ${String(column.language || selectedTable?.language || "ru").toUpperCase()}</i>` : ""}</strong><small>${escapeHtml(column.key)}</small></th>`).join("")}</tr></thead><tbody>${cells || `<tr><td colspan="${columns.length + 1}">Нет строк</td></tr>`}</tbody></table></div>
<div class="hockey-map-table-browser">
<aside class="hockey-map-table-rows">
<header><span>1 · СТРОКА</span><b>${filteredRows.length}</b></header>
<div>${rowButtons || `<div class="hockey-map-table-no-rows">Нет строк</div>`}</div>
</aside>
<section class="hockey-map-table-fields">
<header>
<div><span>2 · ПОЛЯ ВЫБРАННОЙ СТРОКИ</span><strong>${browserRow ? `#${Number(browserRow.index || 0)} · ${escapeHtml(browserRow.label || `Строка ${browserRow.index}`)}` : "Строка не выбрана"}</strong></div>
<b>${columns.length}</b>
</header>
<div class="hockey-map-table-fields-list">${fieldCards || `<div class="hockey-map-table-no-rows">В строке нет доступных полей.</div>`}</div>
<small class="hockey-map-table-fields-hint">Нажмите нужное поле SQL, затем поле vMix — или сначала выберите поле vMix справа.</small>
</section>
</div>
${filteredRows.length > visibleRows.length ? `<small class="hockey-map-table-limit">Показаны первые ${visibleRows.length} из ${filteredRows.length} строк. Используйте поиск строки.</small>` : ""}`}
</details>`;
}
@@ -3340,6 +3359,13 @@
modal.querySelector("[data-map-sql-table-details]")?.addEventListener("toggle", (event) => {
state.mappingSqlTableOpen = Boolean(event.currentTarget.open);
});
modal.querySelectorAll("[data-map-table-row-pick]").forEach((button) => button.addEventListener("click", () => {
state.mappingSelectedTableRow = Number(button.dataset.mapTableRowPick || 1);
state.mappingSelectedTableColumn = "";
state.mappingSelectedSourceKey = "";
state.mappingSqlTableOpen = true;
renderMapping();
}));
// BUILD141: the SQL picker lives inside a clipped flex panel. Keep its own
// scroll position and provide explicit horizontal navigation so every SQL