...

Виджет для системного раздела "Файлы"

Тема в разделе "Примеры решений и дополнительных модулей", создана пользователем Valentin Lysenko, 13 апр 2025.

  1. Valentin Lysenko

    Valentin Lysenko Участник

    Проблема текущего решения
    В стандартном интерфейсе ELMA365 навигация по файловой системе реализована неудобно, что затрудняет работу с вложенными папками и файлами. Мой виджет решает эту проблему, предлагая интуитивно понятное дерево файловой системы с расширенными возможностями.
    Основные возможности
    Интеллектуальное дерево каталогов
    • Иерархическое представление с рекурсивной структурой
    • Интерактивные элементы управления:
      • Кнопки раскрытия/сворачивания ([+]/[-])
      • Различные стили для папок с файлами и без
      • Подсветка активных элементов
    • Автоматическое построение структуры при загрузке
    • Гибкая настройка исключений для системных папок
    Удобный просмотр содержимого
    • Табличное представление файлов с ключевой информацией:
      • Название файла
      • Уникальный идентификатор
      • Размер в байтах (Пока не работает)
    • Адаптивный дизайн с возможностью прокрутки
    • Контекстное отображение:
      • Иконка экспорта только для папок с файлами
      • Визуальные подсказки для пустых директорий
    ⚡ Производительность и оптимизация
    • Ленивая загрузка содержимого (по требованию)
    • Минимизация запросов к серверу
    • Визуализация загрузки для длительных операций
    Экспорт данных
    • Генерация отчетов в формате Excel
    • Автоматическое именование файлов по названию папки
    • Одноразовая загрузка без сохранения временных файлов
    Технические особенности
    • Чистая клиентская реализация без серверных модификаций
    • Гибкая система настроек через конфигурацию
    • Поддержка тем оформления ELMA365
    Преимущества решения
    1. Экономия времени - быстрый доступ к нужным файлам
    2. Наглядность - четкая визуализация структуры
    3. Производительность - оптимизированная работа с большими каталогами
    4. Расширенная функциональность по сравнению со стандартным интерфейсом

    Вложения:

  2. Valentin Lysenko

    Valentin Lysenko Участник

    Спойлер: Виджет код (дерево)
    Код:
    
    <div>
        <% if (
    Context.data.files_tree && Context.data.files_tree.folder_name) { %>
            <
    span><%= Context.data.files_tree.folder_name %></span>
           
            <
    ul>
                <% for (const 
    folder of Context.data.files_tree.sub_folders) { %>
                    <
    li class="folder" id="fldr-<%= folder.folder_id %>" onclick="">
                        <
    span class="folder-id"><%= folder.folder_name %></span>
                       
                        <% if (
    folder.sub_folders && folder.sub_folders.length 0) { %>
                        <
    span class="toggle-btn" onclick="<%= Scripts %>.showFolders('<%= folder.folder_id %>')">[+]</span>
                        <% } %>

                        <
    div class="folder-container"></div>
                    </
    li>

                <% } %>
            </
    ul>

        <% } %>
    </
    div>

    <
    style>
        .
    folder {
            list-
    style-typenone;
            
    margin5px 0;
        }

        .
    pointer {
            
    cursorpointer;
            
    text-decoration-lineunderline;
        }

        .
    sub-folder {
            
    displaynone;
            
    margin-left20px;
            list-
    style-typenone;
        }

        .
    folder-name {
            
    font-weightbold;
        }

        .
    toggle-btn {
            
    margin-right5px;
        }

        .
    get-files-button {
            
    margin-left10px;
            
    border-radius8px;
        }

        .
    link-like:hover {
            
    color#ff0000;         // Цвет при наведении (опционально)
            
    text-decorationnone;  // Убрать подчёркивание при наведении
        
    }

        .
    link-like:active {
            
    color#990000;         // Цвет при клике (эффект нажатия)
            
    transformtranslateY(1px); // Лёгкое смещение для эффекта нажатия
        
    }
        .
    link-like:focus {
            
    outlinenone;          // Убрать стандартный outline
            
    text-decorationdotted underline;
        }
        
    //Пока не работает
        
    .link-like:visited {
            
    color#551a8b;
        
    }

    </
    style>



    Спойлер: Виджет код (загрузка таблицы)
    Код:
    
    <div class="sticky-wrapper">
        <
    div class="table-container">
            <
    svg id='load_file' viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"
                
    width="40" height="40"
                
    style="align-self: end; display: none"
                
    onclick="<%= Scripts %>.exportTableToExcel()">
                <
    g id="_46._File_Download" data-name="46. File Download">
                    <
    path d="m6 45a2 2 0 0 0 2 2h26a2 2 0 0 0 2-2v-32l-6-6h-22a2 2 0 0 0 -2 2z" fill="#dad7e5"/>
                    <
    path d="m36 41h4a2 2 0 0 0 2-2v-32l-6-6h-22a2 2 0 0 0 -2 2v4z" fill="#dad7e5"/>
                    <
    path d="m42 7c0 33.18-.1 32 0 32a4 4 0 0 1 -4-4v-22.83l-7.17-7.17h-11.83a4 4 0 0 1 -4-4h21z" fill="#edebf2"/>
                    <
    path d="m36 13v32h-20a7 7 0 0 1 -7-7v-31h21z" fill="#edebf2"/>
                    <
    path d="m36 13h-6v-6z" fill="#c6c3d8"/>
                    <
    path d="m42 7h-6v-6z" fill="#c6c3d8"/>
                    <
    path d="m30 27a9 9 0 1 1 -12.38-8.34 9 9 0 0 1 12.38 8.34z" fill="#6fabe6"/>
                    <
    path d="m30 27a9 9 0 0 1 -2.62 6.34 8.85 8.85 0 0 1 -3.38.66 9 9 0 0 1 -6.38-15.34 9 9 0 0 1 12.38 8.34z" fill="#82bcf4"/>
                    <
    path d="m25 27-4 5-4-5h2v-5h4v5z" fill="#dad7e5"/>
                    <
    path d="m25 27h-2v-5h-2v5h-2l3 3.75z" fill="#edebf2"/>
                </
    g>
            </
    svg>
            <
    div id='containing_files_table'>
            </
    div>
        </
    div>
    </
    div>



    <
    style>
        
    /* New wrapper styles */
        
    .sticky-wrapper {
            
    positionrelative;
            
    heightcalc(100vh 40px); /* Adjust based on your top value */
        
    }
       
        .
    table-container {
            
    displayflex;
            
    flex-directioncolumn;
            
    positionsticky;
            
    top40px;
            
    height100%; /* Fill the wrapper */
        
    }
       
        .
    table-bordered {
            
    max-height600px;
            
    overflow-yauto;
            
    margin-top0/* Remove any default margins */
        
    }
       
        
    #containing_files_table {
            
    flex-grow1;
            
    displayflex;
            
    flex-directioncolumn;
            
    min-height0/* Important for proper flex child sizing */
        
    }
       
        
    #load_file {
            
    z-index100/* Ensure it stays above content */
            
    backgroundwhite/* Optional: prevent transparency issues */
            
    padding5px/* Visual spacing */
        
    }
    </
    style>

    Спойлер: Клиентский сценарий
    Код:
    
    /* Client scripts module */
    declare const console any
    declare const documentany
    declare const windowany
    import 
    * as XLSX from 'xlsx.full.min.js';

    declare interface 
    SystemFolder {
        
    folder_name string,
        
    folder_idstring,
        
    files?: FileItem[],
        
    sub_foldersSystemFolder[] | undefined
    }


    async function showContext(): Promise<void> {
        
    console.log(Context.data.files_tree)
        
    console.log(Context.data.files_tree.folder_name)
    }
    // Функция для прокликивания всех папок.
    async function checkAllFolders(): Promise<void> {
        const 
    blueButtons document.querySelectorAll('.link-like');
        
    blueButtons.forEach((button: { click: () => any; }) => button.click() )
        
    Context.data.is_checkFolders_button_visible false
    }

    async function buildfiles_tree(): Promise<void> {
        
    //Получаем загрузчик
        
    let loader document.querySelector('.folder-tree-loader')
        
    //Показываем загрузчик
        
    loader.style.display "unset"
        
    //Очищаем переменную
        
    Context.data.files_tree undefined
        
    // Строим иерархию
        
    await build_tree()
        
    // Если список файлов получили сразу - скрываем кнопку. И наоборот.
        
    Context.data.is_checkFolders_button_visible = !Context.data.load_files
        
    //Скрываем загрузчик
        
    loader.style.display "none"
    }
    //Выстраиваем структуру
    async function build_tree(): Promise<void> {
        
    //id системной папки
        
    let system_folder '00000000-0000-0000-0000-000000000000'
        
    // Создаём корень
        
    let files_treeSystemFolder = { folder_name"directory"folder_idsystem_folder sub_folders: [] }
        
    let empty_folder SystemFolder[] = []
        
    // let system_dirs = await System.directories.getDirs(system_folder)

        
    let system_dirs await System.directories.getDirs(system_folder)

        
    //Получаем нормальные имена папок, без полного пути
        
    const regex = /(?<=:)([^:]+)(?=:[^:]*$)/;
        
    //Отфильтровываем папку "shared", так как там папки дублируются и заполняем корень
        
    for (let folder of system_dirs.filter(=> f.data.__name != 'shared' ) ){
            
    let dirty_name folder.data.__name
            
    const match dirty_name.match(regex);
            const 
    f_name match match[0] : dirty_name;
            
    let filesFileItem[] = []
            
    //Если выбрана опция загрузки файлов, загружаем их
            
    if (Context.data.load_files){
               
    files await folder.getFiles()
            }
            const 
    files_tree_parent = {folder_namef_namefolder_idfolder.id sub_foldersempty_folder }
            
    //Получаем подпапки рекурсивно
            
    const sub_folders await getChildFolders(folderfiles_tree_parent)
            
    let root = {folder_namef_namefolder_idfolder.id,  filesfiles  sub_folderssub_folders }
            
    //Добавляем в корень
            
    files_tree.sub_folders!.push(root)
        }
        
    //Записываем в контект для базовой отрисовки
        
    Context.data.files_tree files_tree
    }

    // Получаем папки
    async function getChildFolders(folderDirectoryItemparentSystemFolder): Promise<SystemFolder[] | undefined> {
        try{
            
    let child_dirs await System.directories.getDirs(folder.data.__id)

            
    // console.log(folder.data.__id)
            
    if (!child_dirs || child_dirs.length 1){
                return []
            }
            else{
                const 
    regex = /(?<=:)([^:]+)(?=:[^:]*$)/;
                
    let proper_Folders_arr Context.data.system_folders?.map(fldr => fldr.code)
                
    // for (let folder of child_dirs.filter(f => f.data.__name != 'process' && f.data.__name != 'widgets')){  
                
    for (let folder of child_dirs.filter(=> (proper_Folders_arr?.findIndex(fldr => fldr == f.data.__name) == -1) )){  
                    
    let dirty_name folder.data.__name
                    
    const match dirty_name.match(regex);
                    const 
    f_name match match[0] : dirty_name;
                    
    let filesFileItem[] = []
                    if (
    Context.data.load_files){
                        
    files await folder.getFiles()
                    }
                    
    let empty_folder SystemFolder[] = []
                    const 
    files_tree_parent = {
                        
    folder_namef_name,
                        
    folder_idfolder.id ,
                        
    sub_foldersempty_folder,
                        
    filesfiles
                    
    }
                    
    let sub_folders await getChildFolders(folderfiles_tree_parent)
                   
                    
    parent.sub_folders!.push({
                                
    folder_namef_name,
                                
    folder_idfolder.id,
                                
    filesfiles ,
                                
    sub_folderssub_folders
                            
    })
                    
    // const sub_folders = await getChildFolders(folder)
                   
                
    }
                return 
    parent.sub_folders
            
    }
        }
        catch(
    error){
           
        }
     
    }


    //Отрисовываем папки
    function showFolders(folder_idstring): void {
        
    let dataSystemFolder[] = Context.data.files_tree.sub_folders;
        
    let applicationSystemFolder undefined data.find(folder => folder.folder_id === folder_id);
        
    // Проверяем, что приложение определено
        
    if (!application || !application.sub_folders) return;

        
    let folders application.sub_folders;

        
    // Находим контейнер для добавления папки
        
    let folderContainer document.getElementById(`fldr-${folder_id}`)?.querySelector('.folder-container');
        
    let toggleButton document.getElementById(`fldr-${folder_id}`)?.querySelector('.toggle-btn');

        
    //Прерывание, если не найдено
        
    if (!folderContainer) return;

        
    // Проверяем отрисовку
        
    if (folderContainer.innerHTML === "") {
            
    // Наполняем контейнер папками
            
    let output renderFolders(folders);
            
    folderContainer.innerHTML output;
            
    toggleButton.textContent "[-]"// Функциональный значок
        
    } else {
            
    // Очищаем контейнер
            
    folderContainer.innerHTML "";
            
    toggleButton.textContent "[+]";
        }
        
    setupEventListeners()
    }

    // Отрисовываем папки и подпапки рекурсивно
    function renderFolders(foldersSystemFolder[]): string {
        
    let output "<ul>";
        for (const 
    folder of folders) {

            
    //Если выбрана опция с изначальной загрузкой файлов. Внимание - так будет больше запросов на сервер.
            
    if( Context.data.load_files ){  
                
    //Если файлы присутствуют
                
    if (folder.files && folder.files.length 0){
                    
    output += `<li class="folder pointer" value="${folder.folder_id}" id="folder-${folder.folder_id}">
                        <span class='link-like' value="
    ${folder.folder_id}" >${folder.folder_name}</span>`;
                }
                else{
                    
    output += `<li class="folder" value="${folder.folder_id}" id="folder-${folder.folder_id}">
                        <span class='no-link' value="
    ${folder.folder_id}">${folder.folder_name}</span>`;
                }
            }
            
    // Если не загружаем файлы сразу. Делаем меньше запросов на сервер. Рекомендуемая настройка.
            
    else if( !Context.data.load_files ){
                
    output += `<li class="folder pointer" value="${folder.folder_id}" id="folder-${folder.folder_id}">
                    <span class='link-like' value="
    ${folder.folder_id}" >${folder.folder_name}</span>`;
            }

            if (
    folder.sub_folders && folder.sub_folders.length 0) {
                
    output += `<span class="toggle-btn" data-folder-id="${folder.folder_id}">[+]</span>`;
            }
            
    output += `</li>`;
           
            if (
    folder.sub_folders && folder.sub_folders.length 0) {
                
    output += `<div class="folder-container" id="subfolder-${folder.folder_id}" style="display: none;">${renderFolders(folder.sub_folders)}</div>`;
            }
        }
        
    output += "</ul>";
        return 
    output;
    }

    //функция навешивания обработчиков событий на [+] и <span>
    async function setupEventListeners(): Promise<void> {
        
    setupToggleButtons(); //[+]
        
    setupShowTable() //<span>
    }

    // Навешиваем EventListener на каждую кнопку
    function setupToggleButtons(): void {
        const 
    toggleButtons document.querySelectorAll('.toggle-btn');
        
    //@ts-ignore
        
    toggleButtons.forEach(button => {
            
    button.addEventListener('click', function(eventEvent<any>) {
                
    //@ts-ignore
                
    const folderId = (event.target as HTMLElement).getAttribute('data-folder-id');
                
    showSubFolders(eventfolderId);
            });
        });

        function 
    showSubFolders(eventEventfolderIdstring): void {
            
    //@ts-ignore
            
    event.stopPropagation(); // Предотвращаем вызов родительского элемента
            
    const subfolderContainer document.getElementById(`subfolder-${folderId}`);
            
    //@ts-ignore
            
    const toggleButton event.target as HTMLElement;

            if (
    subfolderContainer) {
                if (
    subfolderContainer.style.display === "none") {
                    
    subfolderContainer.style.display "block"// показать подпапки
                    
    toggleButton.textContent "[-]"// поменять значок
                
    } else {
                    
    subfolderContainer.style.display "none"// скрыть подпапки
                    
    toggleButton.textContent "[+]"// поменять значок
                
    }
            }
        }
    }

    // // Навешиваем EventListener на строчку папки <span>
    function setupShowTable(): void {
        const 
    folder_string document.querySelectorAll('.link-like');
        
    //@ts-ignore
        
    folder_string.forEach(span => {
            
    span.addEventListener('click', function(eventEvent<any>) {
                
    //@ts-ignore
                
    const folder_id = (event.target as HTMLElement).getAttribute('value');
                
    //@ts-ignore
                
    loadFiles(folder_id);
            });
        });
    }

    //Функция отрисовки таблицы
    async function loadFiles(folder_idstring): Promise<void> {

        const 
    button document.querySelector(`[btn-id="${folder_id}"]`)
        
    let files await System.directories.getFiles(folder_id)
        const 
    folder document.getElementById(`folder-${folder_id}`);
        const 
    span folder.querySelector('span')
        
    let folder_name span.innerText;
        
    let dowload_icon document.getElementById('load_file')

        const 
    table_cell document.getElementById(`containing_files_table`);

        
    let output "<div style ='padding: 0px'>";
            if (!
    files || files.length == 0){
               
                
    dowload_icon.style.display "none"
                
    span.classList.remove("link-like");
                
    folder.classList.remove("pointer");
            }
            else if (
    files && files.length 0){
                
    output +=
                `
    <table class="table table-bordered" id='table-${folder_id}' >
                    <h3> 
    ${folder_name} </h3>
                    <thead>
                        <tr class="table-light table customTable">
                            <th>Название файла</th>
                            <th>id файла</th>
                            <th>Вес файла</td>
                        </tr>
                    </thead>
                    <tbody>
    `            
                for (const 
    file of files) {
                    
    output +=  
                    `
    <tr>
                        <td>
    ${file.data.__name }</td>
                        <td>
    ${file.data.__id }</td>
                        <td>
    ${file.data.size } Байт</td>
                    </tr>
    `
                }
                
    output += `
                    </tbody>
                </table>
    `
                
    dowload_icon.style.display "block"
            
    }

        
    output += "</div>";  

        
    table_cell.innerHTML output
       
    }

    //------------------------- Экспорт в файл с использованием min.js------------------------- //
    async function exportTableToExcel(): Promise<void> {
        const 
    table document.querySelector(`#containing_files_table > div > table`);
        const 
    table_name document.querySelector(`#containing_files_table > div > h3`).innerText;

        if (!
    table) {
            
    console.error('Table not found');
            return ;
        }

        
    // Create a new workbook
        
    const workbook XLSX.utils.book_new();

        
    // Convert the HTML table to a worksheet
        
    const worksheet XLSX.utils.table_to_sheet(table);

        
    // Append the worksheet to the workbook
        
    XLSX.utils.book_append_sheet(workbookworksheet'Sheet1');

        
    // Generate a binary string representation of the workbook
        
    const excelBuffer XLSX.write(workbook, {
            
    bookType'xlsx',
            
    type'array'
        
    });
        
    //
        
    let name table_name ".xlsx"
        
    const newTmpFile await System.files.createTemporary(nameexcelBuffer);
        
    let downloadLink await newTmpFile.getDownloadUrl()

        
    Context.data.file newTmpFile
        
    // Create a link element
        
    const link document.createElement('a');
        
    link.href downloadLink
        link
    .download name
        
    // Append to the body (required for Firefox)
        
    document.body.appendChild(link);
        
    link.click(); // Trigger the download

        // Clean up and remove the link
        
    document.body.removeChild(link);
    }
    //------------------------- Экспорт в файл ------------------------- //
  3. Valentin Lysenko

    Valentin Lysenko Участник

    Пример виджета

    [​IMG]Поскольку при формировании дерева скрипт шлёт огромное количество запросов на сервер, запуск скрипта происходит при нажатии на кнопку "Отрисовать дерево", а не в "onInit()".
    Для оптимизации запросов также добавил функционал исключения некоторых системных директорий.
    Загрузка файлов выставлена на "Нет" по той же причине.
    После формирования дерева, все названия папок подчёркнуты. При нажатии происходит проверка на наличие файлов. Если такие есть - справа отобразится таблица. Если содержимого нет - подчёркивание пропадёт.
    Таблицу можно скачать в excel кликнув по иконке. В таблицу вынесено название файла, его id в системе и размер. Размер по каким-то причинам не определяется.
  4. Valentin Lysenko

    Valentin Lysenko Участник

    Прошу добавлять пожелания по доработкам, если такие появятся.
  5. Valentin Lysenko

    Valentin Lysenko Участник

    Добавил лимит на количество строк в таблице для ускорения отрисовки.
    Файл excel теперь формируется на основ JSON, а не таблицы и отражает все элементы.

    Также были найдены ограничения. При большом количестве файлов в папке, необходимо менять лимит ответа на сервере.
    https://elma365.com/ru/help/platform/change-settings-enterprise.html
    elma365.global.maxGrpcMessageSize - Значение по умолчанию: 8388608.
    Ошибка: "Error: grpc: received message larger than max (11002111 vs. 8388608): resource exhausted", , unknown

    Вложения: