import $ from 'jquery'; import {updateIssuesMeta} from './repo-issue.js'; import {toggleElem} from '../utils/dom.js'; import {htmlEscape} from 'escape-goat'; import {Sortable} from 'sortablejs'; function initRepoIssueListCheckboxes() { const $issueSelectAll = $('.issue-checkbox-all'); const $issueCheckboxes = $('.issue-checkbox'); const syncIssueSelectionState = () => { const $checked = $issueCheckboxes.filter(':checked'); const anyChecked = $checked.length !== 0; const allChecked = anyChecked && $checked.length === $issueCheckboxes.length; if (allChecked) { $issueSelectAll.prop({'checked': true, 'indeterminate': false}); } else if (anyChecked) { $issueSelectAll.prop({'checked': false, 'indeterminate': true}); } else { $issueSelectAll.prop({'checked': false, 'indeterminate': false}); } // if any issue is selected, show the action panel, otherwise show the filter panel toggleElem($('#issue-filters'), !anyChecked); toggleElem($('#issue-actions'), anyChecked); // there are two panels but only one select-all checkbox, so move the checkbox to the visible panel $('#issue-filters, #issue-actions').filter(':visible').find('.issue-list-toolbar-left').prepend($issueSelectAll); }; $issueCheckboxes.on('change', syncIssueSelectionState); $issueSelectAll.on('change', () => { $issueCheckboxes.prop('checked', $issueSelectAll.is(':checked')); syncIssueSelectionState(); }); $('.issue-action').on('click', async function (e) { e.preventDefault(); let action = this.getAttribute('data-action'); let elementId = this.getAttribute('data-element-id'); const url = this.getAttribute('data-url'); const issueIDs = $('.issue-checkbox:checked').map((_, el) => { return el.getAttribute('data-issue-id'); }).get().join(','); if (elementId === '0' && url.slice(-9) === '/assignee') { elementId = ''; action = 'clear'; } if (action === 'toggle' && e.altKey) { action = 'toggle-alt'; } updateIssuesMeta( url, action, issueIDs, elementId ).then(() => { window.location.reload(); }).catch((reason) => { window.alert(reason.responseJSON.error); }); }); } function initRepoIssueListAuthorDropdown() { const $searchDropdown = $('.user-remote-search'); if (!$searchDropdown.length) return; let searchUrl = $searchDropdown.attr('data-search-url'); const actionJumpUrl = $searchDropdown.attr('data-action-jump-url'); const selectedUserId = $searchDropdown.attr('data-selected-user-id'); if (!searchUrl.includes('?')) searchUrl += '?'; $searchDropdown.dropdown('setting', { fullTextSearch: true, selectOnKeydown: false, apiSettings: { cache: false, url: `${searchUrl}&q={query}`, onResponse(resp) { // the content is provided by backend IssuePosters handler const processedResults = []; // to be used by dropdown to generate menu items for (const item of resp.results) { let html = `${htmlEscape(item.username)}`; if (item.full_name) html += `${htmlEscape(item.full_name)}`; processedResults.push({value: item.user_id, name: html}); } resp.results = processedResults; return resp; }, }, action: (_text, value) => { window.location.href = actionJumpUrl.replace('{user_id}', encodeURIComponent(value)); }, onShow: () => { $searchDropdown.dropdown('filter', ' '); // trigger a search on first show }, }); // we want to generate the dropdown menu items by ourselves, replace its internal setup functions const dropdownSetup = {...$searchDropdown.dropdown('internal', 'setup')}; const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates'); $searchDropdown.dropdown('internal', 'setup', dropdownSetup); dropdownSetup.menu = function (values) { const $menu = $searchDropdown.find('> .menu'); $menu.find('> .dynamic-item').remove(); // remove old dynamic items const newMenuHtml = dropdownTemplates.menu(values, $searchDropdown.dropdown('setting', 'fields'), true /* html */, $searchDropdown.dropdown('setting', 'className')); if (newMenuHtml) { const $newMenuItems = $(newMenuHtml); $newMenuItems.addClass('dynamic-item'); $menu.append('
', ...$newMenuItems); } $searchDropdown.dropdown('refresh'); // defer our selection to the next tick, because dropdown will set the selection item after this `menu` function setTimeout(() => { $menu.find('.item.active, .item.selected').removeClass('active selected'); $menu.find(`.item[data-value="${selectedUserId}"]`).addClass('selected'); }, 0); }; } function initPinRemoveButton() { for (const button of document.getElementsByClassName('pinned-issue-unpin')) { button.addEventListener('click', async (event) => { const el = event.currentTarget; const id = Number(el.getAttribute('data-issue-id')); // Send the unpin request const response = await fetch(el.getAttribute('data-unpin-url'), { method: 'delete', headers: { 'X-Csrf-Token': window.config.csrfToken, 'Content-Type': 'application/json', }, }); if (response.ok) { // Delete the tooltip el._tippy.destroy(); // Remove the Card el.closest(`div.pinned-issue-card[data-issue-id="${id}"]`).remove(); } }); } } async function pinMoveEnd(e) { const url = e.item.getAttribute('data-move-url'); const id = Number(e.item.getAttribute('data-issue-id')); await fetch(url, { method: 'post', body: JSON.stringify({id, position: e.newIndex + 1}), headers: { 'X-Csrf-Token': window.config.csrfToken, 'Content-Type': 'application/json', }, }); } function initIssuePinSort() { const pinDiv = document.getElementById('issue-pins'); if (pinDiv === null) return; // If the User is not a Repo Admin, we don't need to proceed if (!pinDiv.hasAttribute('data-is-repo-admin')) return; initPinRemoveButton(); // If only one issue pinned, we don't need to make this Sortable if (pinDiv.children.length < 2) return; new Sortable(pinDiv, { group: 'shared', animation: 150, ghostClass: 'card-ghost', onEnd: pinMoveEnd, }); } export function initRepoIssueList() { if (!document.querySelectorAll('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list').length) return; initRepoIssueListCheckboxes(); initRepoIssueListAuthorDropdown(); initIssuePinSort(); }