2023-04-07 00:11:02 +00:00
import $ from 'jquery' ;
2024-07-07 15:32:30 +00:00
import { updateIssuesMeta } from './repo-issue.ts' ;
import { toggleElem , hideElem , isElemHidden } from '../utils/dom.ts' ;
2023-04-07 00:11:02 +00:00
import { htmlEscape } from 'escape-goat' ;
2024-07-07 15:32:30 +00:00
import { confirmModal } from './comp/ConfirmModal.ts' ;
import { showErrorToast } from '../modules/toast.ts' ;
import { createSortable } from '../modules/sortable.ts' ;
import { DELETE , POST } from '../modules/fetch.ts' ;
import { parseDom } from '../utils.ts' ;
2023-04-07 00:11:02 +00:00
function initRepoIssueListCheckboxes() {
2024-03-16 15:08:10 +00:00
const issueSelectAll = document . querySelector ( '.issue-checkbox-all' ) ;
2024-03-17 12:50:32 +00:00
if ( ! issueSelectAll ) return ; // logged out state
2024-03-16 15:08:10 +00:00
const issueCheckboxes = document . querySelectorAll ( '.issue-checkbox' ) ;
2023-04-07 00:11:02 +00:00
const syncIssueSelectionState = ( ) = > {
2024-03-16 15:08:10 +00:00
const checkedCheckboxes = Array . from ( issueCheckboxes ) . filter ( ( el ) = > el . checked ) ;
const anyChecked = Boolean ( checkedCheckboxes . length ) ;
const allChecked = anyChecked && checkedCheckboxes . length === issueCheckboxes . length ;
2023-04-07 00:11:02 +00:00
if ( allChecked ) {
2024-03-16 15:08:10 +00:00
issueSelectAll . checked = true ;
issueSelectAll . indeterminate = false ;
2023-04-07 00:11:02 +00:00
} else if ( anyChecked ) {
2024-03-16 15:08:10 +00:00
issueSelectAll . checked = false ;
issueSelectAll . indeterminate = true ;
2023-04-07 00:11:02 +00:00
} else {
2024-03-16 15:08:10 +00:00
issueSelectAll . checked = false ;
issueSelectAll . indeterminate = false ;
2023-04-07 00:11:02 +00:00
}
// 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
2024-03-16 15:08:10 +00:00
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 ) ;
2023-04-07 00:11:02 +00:00
} ;
2024-03-16 15:08:10 +00:00
for ( const el of issueCheckboxes ) {
el . addEventListener ( 'change' , syncIssueSelectionState ) ;
}
2023-04-07 00:11:02 +00:00
2024-03-16 15:08:10 +00:00
issueSelectAll . addEventListener ( 'change' , ( ) = > {
for ( const el of issueCheckboxes ) {
el . checked = issueSelectAll . checked ;
}
2023-04-07 00:11:02 +00:00
syncIssueSelectionState ( ) ;
} ) ;
$ ( '.issue-action' ) . on ( 'click' , async function ( e ) {
e . preventDefault ( ) ;
2023-06-19 07:46:50 +00:00
const url = this . getAttribute ( 'data-url' ) ;
2023-04-07 00:11:02 +00:00
let action = this . getAttribute ( 'data-action' ) ;
let elementId = this . getAttribute ( 'data-element-id' ) ;
2023-06-19 07:46:50 +00:00
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' ) ) {
2023-04-07 00:11:02 +00:00
elementId = '' ;
action = 'clear' ;
}
2023-06-19 07:46:50 +00:00
// for toggle
2023-04-07 00:11:02 +00:00
if ( action === 'toggle' && e . altKey ) {
action = 'toggle-alt' ;
}
2023-06-19 07:46:50 +00:00
// for delete
if ( action === 'delete' ) {
const confirmText = e . target . getAttribute ( 'data-action-delete-confirm' ) ;
2024-06-07 13:42:31 +00:00
if ( ! await confirmModal ( confirmText , { confirmButtonColor : 'red' } ) ) {
2023-06-19 07:46:50 +00:00
return ;
}
}
2024-02-16 21:41:23 +00:00
try {
await updateIssuesMeta ( url , action , issueIDs , elementId ) ;
2023-04-07 00:11:02 +00:00
window . location . reload ( ) ;
2024-02-16 21:41:23 +00:00
} catch ( err ) {
showErrorToast ( err . responseJSON ? . error ? ? err . message ) ;
}
2023-04-07 00:11:02 +00:00
} ) ;
}
function initRepoIssueListAuthorDropdown() {
const $searchDropdown = $ ( '.user-remote-search' ) ;
if ( ! $searchDropdown . length ) return ;
2024-03-19 23:39:36 +00:00
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' ) ;
2023-04-07 00:11:02 +00:00
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 ) {
2024-03-22 13:45:10 +00:00
let html = ` <img class="ui avatar tw-align-middle" src=" ${ htmlEscape ( item . avatar_link ) } " aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis"> ${ htmlEscape ( item . username ) } </span> ` ;
Migrate margin and padding helpers to tailwind (#30043)
This will conclude the refactor of 1:1 class replacements to tailwind,
except `gt-hidden`. Commands ran:
```bash
perl -p -i -e 's#gt-(p|m)([lrtbxy])?-0#tw-$1$2-0#g' {web_src/js,templates,routers,services}/**/*
perl -p -i -e 's#gt-(p|m)([lrtbxy])?-1#tw-$1$2-0.5#g' {web_src/js,templates,routers,services}/**/*
perl -p -i -e 's#gt-(p|m)([lrtbxy])?-2#tw-$1$2-1#g' {web_src/js,templates,routers,services}/**/*
perl -p -i -e 's#gt-(p|m)([lrtbxy])?-3#tw-$1$2-2#g' {web_src/js,templates,routers,services}/**/*
perl -p -i -e 's#gt-(p|m)([lrtbxy])?-4#tw-$1$2-4#g' {web_src/js,templates,routers,services}/**/*
perl -p -i -e 's#gt-(p|m)([lrtbxy])?-5#tw-$1$2-8#g' {web_src/js,templates,routers,services}/**/*
```
2024-03-24 16:42:49 +00:00
if ( item . full_name ) html += ` <span class="search-fullname tw-ml-2"> ${ htmlEscape ( item . full_name ) } </span> ` ;
2023-04-07 00:11:02 +00:00
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 ) {
2024-03-30 22:14:57 +00:00
const menu = $searchDropdown . find ( '> .menu' ) [ 0 ] ;
// remove old dynamic items
for ( const el of menu . querySelectorAll ( ':scope > .dynamic-item' ) ) {
el . remove ( ) ;
}
2023-04-07 00:11:02 +00:00
const newMenuHtml = dropdownTemplates . menu ( values , $searchDropdown . dropdown ( 'setting' , 'fields' ) , true /* html */ , $searchDropdown . dropdown ( 'setting' , 'className' ) ) ;
if ( newMenuHtml ) {
2024-03-30 22:14:57 +00:00
const newMenuItems = parseDom ( newMenuHtml , 'text/html' ) . querySelectorAll ( 'body > div' ) ;
for ( const newMenuItem of newMenuItems ) {
newMenuItem . classList . add ( 'dynamic-item' ) ;
}
2024-03-16 13:25:27 +00:00
const div = document . createElement ( 'div' ) ;
div . classList . add ( 'divider' , 'dynamic-item' ) ;
2024-03-30 22:14:57 +00:00
menu . append ( div , . . . newMenuItems ) ;
2023-04-07 00:11:02 +00:00
}
$searchDropdown . dropdown ( 'refresh' ) ;
// defer our selection to the next tick, because dropdown will set the selection item after this `menu` function
setTimeout ( ( ) = > {
2024-03-31 11:27:39 +00:00
for ( const el of menu . querySelectorAll ( '.item.active, .item.selected' ) ) {
el . classList . remove ( 'active' , 'selected' ) ;
}
2024-03-30 22:14:57 +00:00
menu . querySelector ( ` .item[data-value=" ${ selectedUserId } "] ` ) ? . classList . add ( 'selected' ) ;
2023-04-07 00:11:02 +00:00
} , 0 ) ;
} ;
}
2023-05-25 13:17:19 +00:00
function initPinRemoveButton() {
2024-06-10 20:49:33 +00:00
for ( const button of document . querySelectorAll ( '.issue-card-unpin' ) ) {
2023-05-25 13:17:19 +00:00
button . addEventListener ( 'click' , async ( event ) = > {
const el = event . currentTarget ;
const id = Number ( el . getAttribute ( 'data-issue-id' ) ) ;
// Send the unpin request
2023-09-19 00:50:30 +00:00
const response = await DELETE ( el . getAttribute ( 'data-unpin-url' ) ) ;
2023-05-25 13:17:19 +00:00
if ( response . ok ) {
// Delete the tooltip
el . _tippy . destroy ( ) ;
// Remove the Card
2023-08-12 10:30:28 +00:00
el . closest ( ` div.issue-card[data-issue-id=" ${ id } "] ` ) . remove ( ) ;
2023-05-25 13:17:19 +00:00
}
} ) ;
}
}
async function pinMoveEnd ( e ) {
const url = e . item . getAttribute ( 'data-move-url' ) ;
const id = Number ( e . item . getAttribute ( 'data-issue-id' ) ) ;
2023-09-19 00:50:30 +00:00
await POST ( url , { data : { id , position : e.newIndex + 1 } } ) ;
2023-05-25 13:17:19 +00:00
}
2023-07-17 18:06:37 +00:00
async function initIssuePinSort() {
2024-06-10 20:49:33 +00:00
const pinDiv = document . querySelector ( '#issue-pins' ) ;
2023-05-25 13:17:19 +00:00
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 ;
2023-07-17 18:06:37 +00:00
createSortable ( pinDiv , {
2023-05-25 13:17:19 +00:00
group : 'shared' ,
2024-08-10 09:46:48 +00:00
onEnd : pinMoveEnd , // eslint-disable-line @typescript-eslint/no-misused-promises
2023-05-25 13:17:19 +00:00
} ) ;
}
2023-10-01 13:04:39 +00:00
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]' ) ;
2023-10-17 14:10:45 +00:00
if ( ! archivedLabels . length ) {
hideElem ( '.archived-label-filter' ) ;
return ;
}
2023-10-01 13:04:39 +00:00
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 ;
} ) ;
}
2023-04-07 00:11:02 +00:00
export function initRepoIssueList() {
if ( ! document . querySelectorAll ( '.page-content.repository.issue-list, .page-content.repository.milestone-issue-list' ) . length ) return ;
initRepoIssueListCheckboxes ( ) ;
initRepoIssueListAuthorDropdown ( ) ;
2023-05-25 13:17:19 +00:00
initIssuePinSort ( ) ;
2023-10-01 13:04:39 +00:00
initArchivedLabelFilter ( ) ;
2023-04-07 00:11:02 +00:00
}