From dd136858f1ea40ad3c94191d647487fa4f31926c Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 18 Oct 2024 20:33:49 +0200 Subject: Adding upstream version 9.0.0. Signed-off-by: Daniel Baumann --- web_src/js/features/repo-issue-list.js | 245 +++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 web_src/js/features/repo-issue-list.js (limited to 'web_src/js/features/repo-issue-list.js') diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js new file mode 100644 index 0000000..92f058c --- /dev/null +++ b/web_src/js/features/repo-issue-list.js @@ -0,0 +1,245 @@ +import $ from 'jquery'; +import {updateIssuesMeta} from './repo-issue.js'; +import {toggleElem, hideElem, isElemHidden} from '../utils/dom.js'; +import {htmlEscape} from 'escape-goat'; +import {confirmModal} from './comp/ConfirmModal.js'; +import {showErrorToast} from '../modules/toast.js'; +import {createSortable} from '../modules/sortable.js'; +import {DELETE, POST} from '../modules/fetch.js'; +import {parseDom} from '../utils.js'; + +function initRepoIssueListCheckboxes() { + const issueSelectAll = document.querySelector('.issue-checkbox-all'); + if (!issueSelectAll) return; // logged out state + const issueCheckboxes = document.querySelectorAll('.issue-checkbox'); + + const syncIssueSelectionState = () => { + const checkedCheckboxes = Array.from(issueCheckboxes).filter((el) => el.checked); + const anyChecked = Boolean(checkedCheckboxes.length); + const allChecked = anyChecked && checkedCheckboxes.length === issueCheckboxes.length; + + if (allChecked) { + issueSelectAll.checked = true; + issueSelectAll.indeterminate = false; + } else if (anyChecked) { + issueSelectAll.checked = false; + issueSelectAll.indeterminate = true; + } else { + issueSelectAll.checked = false; + issueSelectAll.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 + const panels = document.querySelectorAll('#issue-filters, #issue-actions'); + const visiblePanel = Array.from(panels).find((el) => !isElemHidden(el)); + const toolbarLeft = visiblePanel.querySelector('.issue-list-toolbar-left'); + toolbarLeft.prepend(issueSelectAll); + }; + + for (const el of issueCheckboxes) { + el.addEventListener('change', syncIssueSelectionState); + } + + issueSelectAll.addEventListener('change', () => { + for (const el of issueCheckboxes) { + el.checked = issueSelectAll.checked; + } + syncIssueSelectionState(); + }); + + $('.issue-action').on('click', async function (e) { + e.preventDefault(); + + const url = this.getAttribute('data-url'); + let action = this.getAttribute('data-action'); + let elementId = this.getAttribute('data-element-id'); + let issueIDs = []; + for (const el of document.querySelectorAll('.issue-checkbox:checked')) { + issueIDs.push(el.getAttribute('data-issue-id')); + } + issueIDs = issueIDs.join(','); + if (!issueIDs) return; + + // for assignee + if (elementId === '0' && url.endsWith('/assignee')) { + elementId = ''; + action = 'clear'; + } + + // for toggle + if (action === 'toggle' && e.altKey) { + action = 'toggle-alt'; + } + + // for delete + if (action === 'delete') { + const confirmText = e.target.getAttribute('data-action-delete-confirm'); + if (!await confirmModal({content: confirmText, buttonColor: 'orange'})) { + return; + } + } + + try { + await updateIssuesMeta(url, action, issueIDs, elementId); + window.location.reload(); + } catch (err) { + showErrorToast(err.responseJSON?.error ?? err.message); + } + }); +} + +function initRepoIssueListAuthorDropdown() { + const $searchDropdown = $('.user-remote-search'); + if (!$searchDropdown.length) return; + + let searchUrl = $searchDropdown[0].getAttribute('data-search-url'); + const actionJumpUrl = $searchDropdown[0].getAttribute('data-action-jump-url'); + const selectedUserId = $searchDropdown[0].getAttribute('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')[0]; + // remove old dynamic items + for (const el of menu.querySelectorAll(':scope > .dynamic-item')) { + el.remove(); + } + + const newMenuHtml = dropdownTemplates.menu(values, $searchDropdown.dropdown('setting', 'fields'), true /* html */, $searchDropdown.dropdown('setting', 'className')); + if (newMenuHtml) { + const newMenuItems = parseDom(newMenuHtml, 'text/html').querySelectorAll('body > div'); + for (const newMenuItem of newMenuItems) { + newMenuItem.classList.add('dynamic-item'); + } + const div = document.createElement('div'); + div.classList.add('divider', 'dynamic-item'); + menu.append(div, ...newMenuItems); + } + $searchDropdown.dropdown('refresh'); + // defer our selection to the next tick, because dropdown will set the selection item after this `menu` function + setTimeout(() => { + for (const el of menu.querySelectorAll('.item.active, .item.selected')) { + el.classList.remove('active', 'selected'); + } + menu.querySelector(`.item[data-value="${selectedUserId}"]`)?.classList.add('selected'); + }, 0); + }; +} + +function initPinRemoveButton() { + for (const button of document.getElementsByClassName('issue-card-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 DELETE(el.getAttribute('data-unpin-url')); + if (response.ok) { + // Delete the tooltip + el._tippy.destroy(); + // Remove the Card + el.closest(`div.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 POST(url, {data: {id, position: e.newIndex + 1}}); +} + +async 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; + + createSortable(pinDiv, { + group: 'shared', + onEnd: pinMoveEnd, + }); +} + +function initArchivedLabelFilter() { + const archivedLabelEl = document.querySelector('#archived-filter-checkbox'); + if (!archivedLabelEl) { + return; + } + + const url = new URL(window.location.href); + const archivedLabels = document.querySelectorAll('[data-is-archived]'); + + if (!archivedLabels.length) { + hideElem('.archived-label-filter'); + return; + } + const selectedLabels = (url.searchParams.get('labels') || '') + .split(',') + .map((id) => id < 0 ? `${~id + 1}` : id); // selectedLabels contains -ve ids, which are excluded so convert any -ve value id to +ve + + const archivedElToggle = () => { + for (const label of archivedLabels) { + const id = label.getAttribute('data-label-id'); + toggleElem(label, archivedLabelEl.checked || selectedLabels.includes(id)); + } + }; + + archivedElToggle(); + archivedLabelEl.addEventListener('change', () => { + archivedElToggle(); + if (archivedLabelEl.checked) { + url.searchParams.set('archived', 'true'); + } else { + url.searchParams.delete('archived'); + } + window.location.href = url.href; + }); +} + +export function initRepoIssueList() { + if (!document.querySelectorAll('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list').length) return; + initRepoIssueListCheckboxes(); + initRepoIssueListAuthorDropdown(); + initIssuePinSort(); + initArchivedLabelFilter(); +} -- cgit v1.2.3