import { esc } from './esc.js'; import { fs } from './fs.js'; const DEFAULT_RENDER_CONFIG = { labels: { statusReady: 'готов', remove: 'Удалить', empty: 'Нет выбранных файлов', columns: { path: 'Путь', size: 'Размер', status: 'Статус' }, count: (count, bytes) => `${count} файлов · ${fs(bytes)}`, }, layout: { columns: ['path', 'size', 'status'] }, }; function renderConfig(cfg = {}) { return { labels: { ...DEFAULT_RENDER_CONFIG.labels, ...(cfg.labels || {}), columns: { ...DEFAULT_RENDER_CONFIG.labels.columns, ...(cfg.labels?.columns || {}), }, }, layout: { ...DEFAULT_RENDER_CONFIG.layout, ...(cfg.layout || {}) }, }; } /** * Рендерит один узел дерева и его раскрытых потомков в HTML-строки таблицы. * * @param {{kind: string, id: string, name: string, path: string, file?: File, * children: Array, expanded: boolean, error?: string}} node Узел file/folder/zip. * @param {number} depth Глубина узла; используется для визуального отступа. * @returns {string} HTML одной строки или поддерева строк. * * Имена и пути проходят через esc до вставки в innerHTML. Группа получает * кнопку раскрытия, leaf-файл — размер, статус и data-path для будущего API статусов. */ function leafStatuses(node, state, result = []) { if (node.kind === 'file') { result.push(state.statuses.get(node.id) || { kind: 'ready', text: '' }); } else { node.children.forEach((child) => leafStatuses(child, state, result)); } return result; } function renderStatus(node, state, cfg) { if (node.kind === 'file') { const status = state.statuses.get(node.id) || { kind: 'ready', text: cfg.labels.statusReady }; const kind = ['uploading', 'parsing', 'parsed', 'duplicate', 'error'].includes(status.kind) ? status.kind : 'ready'; return `${esc(status.text || cfg.labels.statusReady)}`; } const statuses = leafStatuses(node, state); const errors = statuses.filter((status) => status.kind === 'error').length; const uploading = statuses.filter((status) => status.kind === 'uploading' || status.kind === 'parsing').length; const parsed = statuses.filter((status) => ['parsed', 'duplicate'].includes(status.kind)).length; if (uploading) return `${uploading} загружается/обрабатывается`; if (errors) return `${errors} ошибок`; if (parsed === statuses.length) return `${statuses.length} готово`; return `${parsed}/${statuses.length} обработано`; } function renderNode(node, depth, cfg, state) { const isGroup = node.kind !== 'file'; const padding = depth * 4; const name = esc(node.name); const action = isGroup ? `` : `${name}`; const documentId = state.documents.get(node.id); const details = node.kind === 'file' && documentId ? `` + `` : ''; const remove = ``; const pathAttribute = ` data-path="${esc(node.path)}"`; const fileIdAttribute = node.kind === 'file' ? ` data-file-id="${esc(node.id)}"` : ''; const cells = { path: action, size: node.kind === 'file' ? fs(node.file.size) : '', status: node.error ? esc(node.error) : renderStatus(node, state, cfg), }; const columns = cfg.layout.columns?.length ? cfg.layout.columns : Object.keys(DEFAULT_RENDER_CONFIG.labels.columns); const row = `