Compare commits
5
Commits
fca6f6aafb
...
f7687d3f12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7687d3f12 | ||
|
|
2b202795b8 | ||
|
|
5e2c90f8db | ||
|
|
8316e19725 | ||
|
|
860cdd4214 |
@@ -0,0 +1,37 @@
|
||||
# 2026-09-06: группы слоя 1 по умолчанию свёрнуты
|
||||
|
||||
## Запрос
|
||||
|
||||
При большом количестве файлов в папках и ZIP раскрытая таблица становится неудобной: все файлы сразу занимают много строк, и пользователь вынужден вручную сворачивать группы.
|
||||
|
||||
Требование: папки и ZIP должны быть свёрнуты по умолчанию, с возможностью раскрывать каждую группу отдельно.
|
||||
|
||||
## Найденная причина
|
||||
|
||||
Рендер слоя 1 уже поддерживал `expanded === false`: дочерние строки не выводятся, а кнопка группы показывает стрелку раскрытия. Проблема находилась в создании узлов:
|
||||
|
||||
- корень ZIP и вложенные папки создавались с `expanded: true`;
|
||||
- корни выбранных папок и промежуточные папки также создавались с `expanded: true`.
|
||||
|
||||
## План исправления
|
||||
|
||||
- сделать `expanded: false` только для групп `folder` и `zip`;
|
||||
- оставить leaf-файлы обычными строками;
|
||||
- сохранить независимое ручное раскрытие групп;
|
||||
- добавить регрессионный тест рендера свернутой группы.
|
||||
|
||||
## Результат
|
||||
|
||||
- узлы `folder` и `zip` теперь создаются с `expanded: false`;
|
||||
- leaf-файлы доступны после раскрытия родительской группы;
|
||||
- ручное раскрытие каждой группы сохранено;
|
||||
- добавлен регрессионный тест рендера свернутой папки;
|
||||
- пересобраны ESM и IIFE-бандлы в `dist/`.
|
||||
|
||||
## Проверка
|
||||
|
||||
- `npm run build` выполнен успешно;
|
||||
- `npm test`: `10/10` тестов пройдены;
|
||||
- `python3 -m pytest tests/ -q`: `10/10` тестов пройдены;
|
||||
- `python3 -m py_compile site/app.py` выполнен успешно;
|
||||
- `git diff --check` без замечаний.
|
||||
Vendored
+3
-3
@@ -501,7 +501,7 @@ function safeEntryParts(entryName) {
|
||||
return parts;
|
||||
}
|
||||
function node(kind, name, path, children = [], file = null) {
|
||||
return { id: crypto.randomUUID(), kind, name, path, children, file, expanded: true };
|
||||
return { id: crypto.randomUUID(), kind, name, path, children, file, expanded: kind === "file" };
|
||||
}
|
||||
function addPath(root, parts, fileNode) {
|
||||
let current = root;
|
||||
@@ -745,7 +745,7 @@ function onFolderChange(state, cfg, elements) {
|
||||
name: rootName,
|
||||
path: rootName,
|
||||
children: [],
|
||||
expanded: true
|
||||
expanded: false
|
||||
});
|
||||
}
|
||||
const root = roots.get(rootName);
|
||||
@@ -762,7 +762,7 @@ function onFolderChange(state, cfg, elements) {
|
||||
name: part,
|
||||
path: `${rootName}/${nodeParts.slice(0, index + 1).join("/")}`,
|
||||
children: [],
|
||||
expanded: true
|
||||
expanded: false
|
||||
};
|
||||
current.children.push(child);
|
||||
}
|
||||
|
||||
Vendored
+3
-3
@@ -529,7 +529,7 @@ var FilePicker = (() => {
|
||||
return parts;
|
||||
}
|
||||
function node(kind, name, path, children = [], file = null) {
|
||||
return { id: crypto.randomUUID(), kind, name, path, children, file, expanded: true };
|
||||
return { id: crypto.randomUUID(), kind, name, path, children, file, expanded: kind === "file" };
|
||||
}
|
||||
function addPath(root, parts, fileNode) {
|
||||
let current = root;
|
||||
@@ -773,7 +773,7 @@ var FilePicker = (() => {
|
||||
name: rootName,
|
||||
path: rootName,
|
||||
children: [],
|
||||
expanded: true
|
||||
expanded: false
|
||||
});
|
||||
}
|
||||
const root = roots.get(rootName);
|
||||
@@ -790,7 +790,7 @@ var FilePicker = (() => {
|
||||
name: part,
|
||||
path: `${rootName}/${nodeParts.slice(0, index + 1).join("/")}`,
|
||||
children: [],
|
||||
expanded: true
|
||||
expanded: false
|
||||
};
|
||||
current.children.push(child);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "upload-platform-file-picker",
|
||||
"private": true,
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "node build.mjs",
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ try:
|
||||
except (OSError, json.JSONDecodeError):
|
||||
CONFIG = DEFAULT_CONFIG
|
||||
|
||||
VERSION = "0.2.1"
|
||||
VERSION = "0.2.2"
|
||||
|
||||
app = Flask(
|
||||
__name__,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { render } from '../upload/frontend/table/render.js';
|
||||
|
||||
function elements() {
|
||||
return { tableBodyEl: { innerHTML: '' }, countEl: { textContent: '' } };
|
||||
}
|
||||
|
||||
function fileNode() {
|
||||
return {
|
||||
id: 'file-1',
|
||||
kind: 'file',
|
||||
name: 'document.txt',
|
||||
path: 'folder/document.txt',
|
||||
file: { size: 7 },
|
||||
children: [],
|
||||
expanded: true,
|
||||
};
|
||||
}
|
||||
|
||||
test('render: папка по умолчанию скрывает дочерние файлы', () => {
|
||||
const child = fileNode();
|
||||
const state = {
|
||||
nodes: [{
|
||||
id: 'folder-1',
|
||||
kind: 'folder',
|
||||
name: 'folder',
|
||||
path: 'folder',
|
||||
children: [child],
|
||||
expanded: false,
|
||||
}],
|
||||
};
|
||||
const output = elements();
|
||||
|
||||
render(state, output);
|
||||
|
||||
assert.match(output.tableBodyEl.innerHTML, /data-toggle="folder-1"/);
|
||||
assert.match(output.tableBodyEl.innerHTML, /aria-expanded="false"/);
|
||||
assert.doesNotMatch(output.tableBodyEl.innerHTML, /data-path="folder\/document\.txt"/);
|
||||
assert.equal(output.countEl.textContent, '1 файлов · 7 B');
|
||||
|
||||
state.nodes[0].expanded = true;
|
||||
render(state, output);
|
||||
|
||||
assert.match(output.tableBodyEl.innerHTML, /data-path="folder\/document\.txt"/);
|
||||
assert.match(output.tableBodyEl.innerHTML, /aria-expanded="true"/);
|
||||
});
|
||||
@@ -31,7 +31,7 @@ export function onFolderChange(state, cfg, elements) {
|
||||
if (!roots.has(rootName)) {
|
||||
// Один root на выбранный каталог позволяет сохранить дерево целиком.
|
||||
roots.set(rootName, { id: crypto.randomUUID(), kind: 'folder', name: rootName,
|
||||
path: rootName, children: [], expanded: true });
|
||||
path: rootName, children: [], expanded: false });
|
||||
}
|
||||
const root = roots.get(rootName);
|
||||
// Вставляет узел по его пути, создавая отсутствующие промежуточные папки.
|
||||
@@ -44,7 +44,7 @@ export function onFolderChange(state, cfg, elements) {
|
||||
if (!child) {
|
||||
child = last ? node : { id: crypto.randomUUID(), kind: 'folder', name: part,
|
||||
path: `${rootName}/${nodeParts.slice(0, index + 1).join('/')}`,
|
||||
children: [], expanded: true };
|
||||
children: [], expanded: false };
|
||||
current.children.push(child);
|
||||
}
|
||||
current = child;
|
||||
|
||||
@@ -36,9 +36,9 @@ function safeEntryParts(entryName) {
|
||||
return parts;
|
||||
}
|
||||
|
||||
/** Создаёт единый узел file, folder или zip с уникальным id и раскрытым состоянием. */
|
||||
/** Создаёт единый узел file, folder или zip с уникальным id и начальным состоянием раскрытия. */
|
||||
function node(kind, name, path, children = [], file = null) {
|
||||
return { id: crypto.randomUUID(), kind, name, path, children, file, expanded: true };
|
||||
return { id: crypto.randomUUID(), kind, name, path, children, file, expanded: kind === 'file' };
|
||||
}
|
||||
|
||||
/** Вставляет leaf или вложенное дерево по сегментам пути, создавая folder-узлы. */
|
||||
|
||||
Reference in New Issue
Block a user