/
amesk
/
gitlab-analyzer
Обзор
Документация
Войти
/
amesk
/
gitlab-analyzer
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
jquery
renderer.js
710 строк
26 KB
amesk
Cosmetics.
15 фев 2025, 20:24
15 фев 2025, 20:24
97f3c78
Код
Авторство
О чём код?
function extractSortCritera(query) { // Регулярное выражение для поиска фрагмента "sort by" const sortByRegex = /(.*)sort by\s+([\w\s,]+)\s+(asc|desc)\s*$/i; // Проверяем, есть ли в запросе фрагмент "sort by" const match = query.match(sortByRegex); if (!match) { // Если фрагмент "sort by" не найден, возвращаем исходный запрос и пустые // значения для сортировки return { searchQuery: query.trim(), sortFields: ["iid"], sortDirection: "asc", }; } // Извлекаем части запроса const searchQuery = match[1].trim(); // Поисковый запрос до "sort by" const fields = match[2].split(',').map((field) => field.trim()); // Поля для сортировки const sortDirection = match[3].toLowerCase(); // Направление сортировки (asc или desc) return { searchQuery, sortFields: fields, sortDirection, }; } function filterIssues(issues, fullExpression) { const preParsed = extractSortCritera(fullExpression); const expression = preParsed.searchQuery; const sortFields = preParsed.sortFields; const sortDirection = preParsed.sortDirection === "asc" ? 1 : -1; const jexl = new Jexl.Jexl(); function analyzeIssue(issue) { const labels_has = (target) => { if (typeof target === 'string') { return issue.labels.some(label => label[0] === target); } else if (Array.isArray(target)) { return target.some(text => issue.labels.some(label => label[0] === text)); } return false; }; jexl.addFunction("has", labels_has); jexl.addFunction("any", labels_has); const labels_all = (target) => { if (Array.isArray(target)) { return target.every(text => issue.labels.some(label => label[0] === text)); } return false; }; jexl.addFunction("every", labels_all); jexl.addFunction("empty", (field)=>field == undefined); const context = { iid: issue.iid, title: issue.title, assignee: issue.assignee, milestone: issue.milestone, priority: ~~(issue.priority), estimation: { empty: issue.estimation === undefined || issue.estimation == 0, seconds: ~~(issue.estimation), weeks: ~~(issue.estimation / (60 * 60 * 8 * 5)), days: ~~(issue.estimation / (60 * 60 * 8)), hours: ~~(issue.estimation / (60 * 60)), minutes: ~~(issue.estimation / 60) }, time_spent: { empty: issue.total_time_spent === undefined || issue.total_time_spent == 0, seconds: ~~(issue.total_time_spent), weeks: ~~(issue.total_time_spent / (60 * 60 * 8 * 5)), days: ~~(issue.total_time_spent / (60 * 60 * 8)), hours: ~~(issue.total_time_spent / (60 * 60)), minutes: ~~(issue.total_time_spent / 60) }, age: { seconds: ~~(issue.age), months: ~~(issue.age / (60 * 60 * 24 * 30)), weeks: ~~(issue.age / (60 * 60 * 24 * 7)), days: ~~(issue.age / (60 * 60 * 24)), hours: ~~(issue.age / (60 * 60)), minutes: ~~(issue.age / 60) }, updated: { seconds: ~~(issue.updated), months: ~~(issue.updated / (60 * 60 * 24 * 30)), weeks: ~~(issue.updated / (60 * 60 * 24 * 7)), days: ~~(issue.updated / (60 * 60 * 24)), hours: ~~(issue.updated / (60 * 60)), minutes: ~~(issue.updated / 60) }, title: issue.title, labels: { empty: issue.labels.length === 0, has: (targetText) => labels_has(targetText), any: (targetText) => labels_has(targetText), all: (targetText) => labels_all(targetText) } }; if (expression === "") { return issue; } else { const match = jexl.evalSync(expression, context); return match ? issue : null; } } const filtered = issues.map(analyzeIssue).filter(issue => issue !== null); if (sortFields) { filtered.sort((a, b) => { for (const field of sortFields) { if (a[field] > b[field]) { return sortDirection; } if (a[field] < b[field]) { return -sortDirection; } } return 0; }); } return filtered; } // Сохраняем выбранные колонки в localStorage function saveColumnSelection(columns) { localStorage.setItem('selectedColumns', JSON.stringify(columns)); } // Загружаем выбранные колонки из localStorage function loadColumnSelection() { const savedColumns = localStorage.getItem('selectedColumns'); return savedColumns ? JSON.parse(savedColumns) : { id: true, title: true, labels: true, assignee: true, milestone: true, age: true, lastUpdated: true, estimation: true, timeSpent: true }; } // Применяем выбранные колонки к таблице function applyColumnSelection(columns) { const columnMap = { id: 2, title: 3, labels: 4, assignee: 5, milestone: 6, age: 7, lastUpdated: 8, estimation: 9, timeSpent: 10 }; // Скрываем или показываем колонки $.each(columns, function(column, isVisible) { const index = columnMap[column]; if (index) { const header = $(`.table th:nth-child(${index})`); const cells = $(`.table td:nth-child(${index})`); header.toggleClass('hidden-column', !isVisible); cells.toggleClass('hidden-column', !isVisible); } }); } function getSelectedColumns() { const selectedColumns = {}; $('#columnSelectorForm').find('input[type="checkbox"]').each(function() { selectedColumns[$(this).val()] = $(this).is(':checked'); }); return selectedColumns; } function reapllyColumnSelection() { const selectedColumns = getSelectedColumns(); applyColumnSelection(selectedColumns); } // Инициализация выбора колонок function initColumnSelector() { const modal = $('#columnSelectorModal'); const columnSelectorForm = $('#columnSelectorForm'); // Открываем модальное окно $('#columnSelectorButton').on('click', function() { modal.css('display', 'flex'); }); // Закрываем модальное окно при клике вне его $(window).on('click', function(event) { if (event.target === modal[0]) { modal.css('display', 'none'); } }); // Применяем выбранные колонки $('#applyColumnSelection').on('click', function() { const selectedColumns = getSelectedColumns(); saveColumnSelection(selectedColumns); applyColumnSelection(selectedColumns); modal.css('display', 'none'); }); // Загружаем сохраненные настройки const savedColumns = loadColumnSelection(); applyColumnSelection(savedColumns); columnSelectorForm.find('input[type="checkbox"]').each(function() { $(this).prop('checked', savedColumns[$(this).val()]); }); } function dropIssuesSelections() { // Сбрасываем selectedIssues $('#selectAllCheckbox').prop('checked', false); $('.issue-checkbox').prop('checked', this.checked).trigger('change'); } function showApiTokenDialog() { const identifiers = ['projectId', 'searchInput', 'loadIssues', 'logoutButton', 'helpButton', 'columnSelectorButton']; $(identifiers.map(id => `#${id}`).join(', ')).prop('disabled', true); const modal = $('#apiKeyModal'); modal.css('display', 'flex'); $('#submitApiKey').on('click', () => { const apiKey = $('#apiKeyInput').val().trim(); if (apiKey) { localStorage.setItem('token', apiKey); window.electronAPI.saveToken(apiKey); hideApiTokenDialog(); } else { reportError('Please enter a valid API Key.'); } }); } function hideApiTokenDialog() { $('#apiKeyModal').hide(); const identifiers = ['projectId', 'searchInput', 'loadIssues', 'logoutButton', 'helpButton', 'columnSelectorButton']; $(identifiers.map(id => `#${id}`).join(', ')).prop('disabled', false); } function reportCommon(msg, boostrapCls) { const div = $('<div>').addClass(`alert ${boostrapCls}`).text(msg); $('body').prepend(div); setTimeout(() => div.remove(), 3000); setTimeout(() => { $(this).focus(); const length = $(this).val().length; $(this).get(0).setSelectionRange(length, length); }, 50); } function reportError(msg) { reportCommon(msg, 'alert-danger'); } function reportSuccess(msg) { reportCommon(msg, 'alert-success'); } $(document).ready(function () { // Инициализация tooltips $('[data-bs-toggle="tooltip"]').each(function () { new bootstrap.Tooltip(this); }); $('#projectId').select2({ placeholder: "Enter project Id or select from the list", // Плейсхолдер allowClear: true, // Разрешить очистку выбора tags: true, // Разрешить добавление новых вариантов data: [ { id: 'navigation/uninav', text: 'navigation/uninav'}, { id: 'navigation/dms-gateway', text: 'navigation/dms-gateway'}, { id: 'navigation/weekly-report', text: 'navigation/weekly-report'}, { id: 'techsim/techsim-issues-board', text: 'techsim/techsim-issues-board'} ] }); $('#label').select2({ placeholder: "Enter label or select from the list", // Плейсхолдер allowClear: true, // Разрешить очистку выбора tags: true // Разрешить добавление новых вариантов }); // Обработчик события select2:select $('#projectId').on('select2:select', function (e) { const selectedData = e.params.data; $('#select2-projectId-container').removeClass('is-invalid'); // Получаем текущий список данных Select2 const currentData = $('#projectId').select2('data'); if (currentData) { // Проверяем, есть ли выбранное значение в исходном списке данных const isNewValue = !currentData.some(item => item.id === selectedData.id); // Если значение новое, добавляем его в список данных if (isNewValue) { const newValue = selectedData.id; // Введенное значение if (newValue) { // Добавляем новое значение в список данных const newOption = new Option(newValue, newValue, true, true); $('#projectId').append(newOption).trigger('change'); } } else { // Обновляем значение элемента $(this).val(selectedData.id).trigger('change'); } } }); // Обработчик для загрузки issues при выборе проекта $('#loadIssues').on('click', function () { const projectId = $('#projectId').val(); if (projectId) { loadIssues(projectId); } else { reportError('Please select a project.'); } }); let allIssues = []; let selectedIssues = new Set(); function setLoadingState(isLoading) { const identifiers = ['projectId', 'searchInput', 'loadIssues', 'logoutButton', 'helpButton', 'columnSelectorButton']; $(identifiers.map(id => `#${id}`).join(', ')).prop('disabled', isLoading); $('#loadingSpinnerModal').css('display', isLoading? 'flex' : 'none'); } function updateFormState() { $('#label').prop('disabled', selectedIssues.size === 0); $('#addLabelButton').prop('disabled', selectedIssues.size === 0); $('#removeLabelButton').prop('disabled', selectedIssues.size === 0); $('#closeIssues').prop('disabled', selectedIssues.size === 0); $('#exportToExcelButton').prop('disabled', selectedIssues.size === 0); } async function loadIssues() { const projectIdInput = $('#projectId'); $('#searchInput').empty(); let projectId = projectIdInput.val(); if (!projectId) { $('#select2-projectId-container').addClass('is-invalid'); return; } $('#select2-projectId-container').removeClass('is-invalid'); setLoadingState(true); try { const result = await window.electronAPI.getOpenIssues(projectId); if (result.success) { allIssues = filterIssues(result.issues, ""); renderIssues(allIssues); } else { $('#select2-projectId-container').addClass('is-invalid'); reportError('Error loading issues'); } } catch (error) { $('#select2-projectId-container').addClass('is-invalid'); console.error('Error loading issues:', error); projectIdInput.addClass('is-invalid'); } finally { setLoadingState(false); updateFormState(); } } async function updateLabelList() { const projectIdInput = $('#projectId'); let projectId = projectIdInput.val(); let projectLabels = []; try { const result = await window.electronAPI.getProjectLabels(projectId); if (result.success) { projectLabels = result.labels; } else { console.error('Error updating labels!'); return; } const label = $('#label').empty(); // Очищаем текущий список for (l in projectLabels) { const newOption = new Option(projectLabels[l].name, projectLabels[l].name, true, true); label.append(newOption).trigger('change'); } const newOption = new Option('', '', true, true); label.append(newOption).trigger('change'); } catch (error) { console.error('Error updateing labels:', error); } } function formatTimeCommon(seconds, day, week, month) { const secondsInHour = 3600; const secondsInDay = day * secondsInHour; const secondsInWeek = week * secondsInDay; const secondsInMonth = month * secondsInDay; let months = Math.floor(seconds / secondsInMonth); let remaining = seconds % secondsInMonth; let weeks = Math.floor(remaining / secondsInWeek); remaining = seconds % secondsInWeek; let days = Math.floor(remaining / secondsInDay); remaining = remaining % secondsInDay; let hours = Math.floor(remaining / secondsInHour); // Формируем строку, исключая нулевые компоненты let result = []; if (months > 0) result.push(`${months}m`); if (weeks > 0) result.push(`${weeks}w`); if (days > 0) result.push(`${days}d`); if (hours > 0) result.push(`${hours}h`); // Объединяем компоненты в строку return result.join(' '); } function formatWorkTime(seconds) { return formatTimeCommon(seconds, 8, 5, 22); } function formatCalendarTime(seconds) { return formatTimeCommon(seconds, 24, 7, 30); } function renderIssues(issues) { const issuesList = $('#issues').empty(); issues.forEach(issue => { const row = $('<tr>').attr('data-issue-id', issue.iid); const spentTime = issue.total_time_spent ? formatWorkTime(issue.total_time_spent) : ''; const estimatedTime = issue.estimation ? formatWorkTime(issue.estimation) : ''; const age = formatCalendarTime(issue.age); const updated = formatCalendarTime(issue.updated); row.html(` <td><input type="checkbox" class="issue-checkbox" value="${issue.title}"></td> <td scope="row"><b>${issue.iid}</b></td> <td title="${issue.title}"><a href="${issue.url}">${issue.title}</a></td> <td>${issue.labels.map(label => `<span class="badge" style="background-color: ${label[1]};">${label[0]}</span>`).join(' ')}</td> <td>${issue.assignee || ''}</td> <td>${issue.milestone || ''}</td> <td>${age}</td> <td>${updated}</td> <td>${estimatedTime || ''}</td> <td>${spentTime || ''}</td> `); row.find('.issue-checkbox').on('change', function () { if (this.checked) { selectedIssues.add(issue.iid); } else { selectedIssues.delete(issue.iid); } updateFormState(); }); issuesList.append(row); reapllyColumnSelection(); }); // Обновляем состояние "Выбрать все" $('#selectAllCheckbox').prop('checked', false); } $('#loadIssues').on('click', async ()=>{ await loadIssues(); await updateLabelList(); }); $('#selectAllCheckbox').on('change', function () { $('.issue-checkbox').prop('checked', this.checked).trigger('change'); }); $('#searchInput').on('keydown', async function (event) { if (event.key != 'Enter') { return; } dropIssuesSelections(); const searchTerm = $(this).val().trim(); if (!searchTerm) { renderIssues(allIssues); return; } try { const filteredIssues = filterIssues(allIssues, searchTerm); renderIssues(filteredIssues); } catch (e) { // Если произошла ошибка, показываем сообщение об ошибке console.log(e); reportError('Invalid condition. Please check your input.'); } }); async function modifyLabel(action) { const issueIds = Array.from(selectedIssues); const label = $('#label').val(); let projectLabels = {}; if (!label) { reportError('Please select a label.'); return; } if (selectedIssues.length === 0) { reportError('Please select at least one issue.'); return; } const projectId = $('#projectId').val(); const result = await window.electronAPI.getProjectLabels(projectId); if (result.success) { projectLabels = new Map(result.labels.map(item=>[item.name, item.color])); } else { reportError('Error updating labels!'); return; } // Показываем спиннер загрузки setLoadingState(true); try { // Выполняем операцию обновления метки для каждой выбранной задачи for (const issueId of selectedIssues) { const result = await window.electronAPI.updateLabel(projectId, issueId, label, action); if (!result.success) { throw new Error(result.error); } // Локально обновляем метку в allIssues const issue = allIssues.find(issue => issue.iid === issueId); if (issue) { if (action === 'add') { if (!issue.labels.some(l => l[0] === label)) { issue.labels.push([label, projectLabels.get(label) || '#CCCCCC']); // Добавляем новую метку } } else if (action === 'remove') { issue.labels = issue.labels.filter(l => l[0] !== label); // Удаляем метку } } selectedIssues.delete(issueId); } // Берём строку поиска и заново всё перефильтровываем const searchTerm = $('#searchInput').val().trim(); const filteredIssues = filterIssues(allIssues, searchTerm); // Перерисовываем таблицу с обновленными данными renderIssues(filteredIssues); // Убираем чекбоксы dropIssuesSelections(); // Уведомляем пользователя reportSuccess('Labels updated.'); } catch (error) { // Скрываем спиннер загрузки в случае ошибки setLoadingState(false); updateFormState(); reportError(`Error updating label: ${error.message}`); } finally { // Скрываем спиннер загрузки setLoadingState(false); updateFormState(); } } // Обработчик для кнопки "Remove label" $('#removeLabelButton').on('click', async function () { await modifyLabel('remove'); }); // Обработчик для кнопки "Add label" $('#addLabelButton').on('click', async function () { await modifyLabel('add'); }); $('#projectId, #searchInput').on('input', function () { $(this).removeClass('is-invalid'); }); $('#helpButton').on('click', () => { window.electronAPI.openHelpWindow(); }); $(document).on('click', 'a[href^="http"]', function (event) { event.preventDefault(); const url = $(this).attr('href'); window.electronAPI.openExternalLink(url); }); $('#openGitLab').on('click', () => { const projectId = $('#projectId').val(); const gitlabUrl = `${window.electronAPI.GITLAB_URL}/${projectId}/-/issues`; window.electronAPI.openExternalLink(gitlabUrl); }); $('#logoutButton').on('click', async () => { try { window.electronAPI.saveToken(null); showApiTokenDialog(); } catch (error) { console.error('Error during logout:', error); } }); $('#closeIssues').on('click', async () => { const selectedIds = Array.from(selectedIssues); if (selectedIds.length === 0) { reportError('Please select at least one issue to close.'); return; } const projectId = $('#projectId').val(); if (!projectId) { reportError('Please select a project.'); return; } if (confirm("Are you sure you want to close these issues: " + selectedIds.join(', ') + '?')) { const result = await window.electronAPI.closeIssues(projectId, selectedIssues); if (result.success) { // Берём строку поиска и заново всё перефильтровываем const searchTerm = $('#searchInput').val().trim(); const filteredIssues = filterIssues(allIssues, searchTerm); // Перерисовываем таблицу с обновленными данными renderIssues(filteredIssues); // Убираем чекбоксы dropIssuesSelections(); } else { reportError(`Failed to close issues: ${result.error}`); } } }); $('#exportToExcelButton').on('click', async () => { const selectedIds = Array.from(selectedIssues); result = []; selectedColumns = getSelectedColumns(); allIssues.forEach(issue => { if (selectedIds.includes(issue.iid)) { const spentTime = issue.total_time_spent ? formatWorkTime(issue.total_time_spent) : ''; const estimatedTime = issue.estimation ? formatWorkTime(issue.estimation) : ''; const age = formatCalendarTime(issue.age); const updated = formatCalendarTime(issue.updated); const entry = { id: issue.iid, title: issue.title, labels: issue.labels.map(label => label[0]).join(', '), assignee: issue.assignee, milestone: issue.milestone, age: age, updated: updated, estimatedTime: estimatedTime, spentTime: spentTime, }; for (key in entry) { if (!selectedColumns[key]) { delete entry[key]; } } result.push(entry); } }); window.electronAPI.exportToExcel(result); }); const token = localStorage.getItem('token'); if (!token) { showApiTokenDialog(); } else { window.electronAPI.saveToken(token); hideApiTokenDialog(); } initColumnSelector(); updateFormState(); });