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 = ` <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}/**/*
```
8 months ago
if ( item . full _name ) html += ` <span class="search-fullname tw-ml-2"> ${ htmlEscape ( item . full _name ) } </span> ` ;
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 ( ) ;
}