@ -1,10 +1,13 @@
import './publicpath.js' ;
import Vue from 'vue' ;
import { htmlEscape } from 'escape-goat' ;
import 'jquery.are-you-sure' ;
import ActivityTopAuthors from './components/ActivityTopAuthors.vue' ;
import { initVueEnv } from './components/VueComponentLoader.js' ;
import { initRepoActivityTopAuthorsChart } from './components/RepoActivityTopAuthors.vue' ;
import { initDashboardRepoList } from './components/DashboardRepoList.js' ;
import { initRepoBranchTagDropdown } from './components/RepoBranchTagDropdown.js' ;
import attachTribute from './features/tribute.js' ;
import createColorPicker from './features/colorpicker.js' ;
import createDropzone from './features/dropzone.js' ;
@ -27,20 +30,16 @@ import {initStopwatch} from './features/stopwatch.js';
import { showLineButton } from './code/linebutton.js' ;
import { initMarkupContent , initCommentContent } from './markup/content.js' ;
import { stripTags , mqBinarySearch } from './utils.js' ;
import { svg , svgs } from './svg.js' ;
import { svg } from './svg.js' ;
const { AppSubUrl , AssetUrlPrefix , csrf } = window . config ;
const { AppSubUrl , csrf } = window . config ;
let previewFileModes ;
const commentMDEditors = { } ;
// Silence fomantic's error logging when tabs are used without a target content element
$ . fn . tab . settings . silent = true ;
// Silence Vue's console advertisements in dev mode
// To use the Vue browser extension, enable the devtools option temporarily
Vue . config . productionTip = false ;
Vue . config . devtools = false ;
initVueEnv ( ) ;
function initCommentPreviewTab ( $form ) {
const $tabMenu = $form . find ( '.tabular.menu' ) ;
@ -806,7 +805,7 @@ async function initRepository() {
// File list and commits
if ( $ ( '.repository.file.list' ) . length > 0 ||
$ ( '.repository.commits' ) . length > 0 || $ ( '.repository.release' ) . length > 0 ) {
initFilter BranchTagDropdown ( '.choose.reference .dropdown' ) ;
initRepo BranchTagDropdown ( '.choose.reference .dropdown' ) ;
}
// Wiki
@ -2852,7 +2851,8 @@ $(document).ready(async () => {
initWebhook ( ) ;
initAdmin ( ) ;
initCodeView ( ) ;
initVueApp ( ) ;
initRepoActivityTopAuthorsChart ( ) ;
initDashboardRepoList ( ) ;
initTeamSettings ( ) ;
initCtrlEnterSubmit ( ) ;
initNavbarContentToggle ( ) ;
@ -3099,369 +3099,6 @@ function linkEmailAction(e) {
e . preventDefault ( ) ;
}
function initVueComponents ( ) {
// register svg icon vue components, e.g. <octicon-repo size="16"/>
for ( const [ name , htmlString ] of Object . entries ( svgs ) ) {
const template = htmlString
. replace ( /height="[0-9]+"/ , 'v-bind:height="size"' )
. replace ( /width="[0-9]+"/ , 'v-bind:width="size"' ) ;
Vue . component ( name , {
props : {
size : {
type : String ,
default : '16' ,
} ,
} ,
template ,
} ) ;
}
const vueDelimeters = [ '${' , '}' ] ;
Vue . component ( 'repo-search' , {
delimiters : vueDelimeters ,
props : {
searchLimit : {
type : Number ,
default : 10
} ,
suburl : {
type : String ,
required : true
} ,
uid : {
type : Number ,
required : true
} ,
teamId : {
type : Number ,
required : false ,
default : 0
} ,
organizations : {
type : Array ,
default : ( ) => [ ] ,
} ,
isOrganization : {
type : Boolean ,
default : true
} ,
canCreateOrganization : {
type : Boolean ,
default : false
} ,
organizationsTotalCount : {
type : Number ,
default : 0
} ,
moreReposLink : {
type : String ,
default : ''
}
} ,
data ( ) {
const params = new URLSearchParams ( window . location . search ) ;
let tab = params . get ( 'repo-search-tab' ) ;
if ( ! tab ) {
tab = 'repos' ;
}
let reposFilter = params . get ( 'repo-search-filter' ) ;
if ( ! reposFilter ) {
reposFilter = 'all' ;
}
let privateFilter = params . get ( 'repo-search-private' ) ;
if ( ! privateFilter ) {
privateFilter = 'both' ;
}
let archivedFilter = params . get ( 'repo-search-archived' ) ;
if ( ! archivedFilter ) {
archivedFilter = 'unarchived' ;
}
let searchQuery = params . get ( 'repo-search-query' ) ;
if ( ! searchQuery ) {
searchQuery = '' ;
}
let page = 1 ;
try {
page = parseInt ( params . get ( 'repo-search-page' ) ) ;
} catch {
// noop
}
if ( ! page ) {
page = 1 ;
}
return {
tab ,
repos : [ ] ,
reposTotalCount : 0 ,
reposFilter ,
archivedFilter ,
privateFilter ,
page ,
finalPage : 1 ,
searchQuery ,
isLoading : false ,
staticPrefix : AssetUrlPrefix ,
counts : { } ,
repoTypes : {
all : {
searchMode : '' ,
} ,
forks : {
searchMode : 'fork' ,
} ,
mirrors : {
searchMode : 'mirror' ,
} ,
sources : {
searchMode : 'source' ,
} ,
collaborative : {
searchMode : 'collaborative' ,
} ,
}
} ;
} ,
computed : {
showMoreReposLink ( ) {
return this . repos . length > 0 && this . repos . length < this . counts [ ` ${ this . reposFilter } : ${ this . archivedFilter } : ${ this . privateFilter } ` ] ;
} ,
searchURL ( ) {
return ` ${ this . suburl } /api/v1/repos/search?sort=updated&order=desc&uid= ${ this . uid } &team_id= ${ this . teamId } &q= ${ this . searchQuery
} & page = $ { this . page } & limit = $ { this . searchLimit } & mode = $ { this . repoTypes [ this . reposFilter ] . searchMode
} $ { this . reposFilter !== 'all' ? '&exclusive=1' : ''
} $ { this . archivedFilter === 'archived' ? '&archived=true' : '' } $ { this . archivedFilter === 'unarchived' ? '&archived=false' : ''
} $ { this . privateFilter === 'private' ? '&is_private=true' : '' } $ { this . privateFilter === 'public' ? '&is_private=false' : ''
} ` ;
} ,
repoTypeCount ( ) {
return this . counts [ ` ${ this . reposFilter } : ${ this . archivedFilter } : ${ this . privateFilter } ` ] ;
}
} ,
mounted ( ) {
this . changeReposFilter ( this . reposFilter ) ;
$ ( this . $el ) . find ( '.poping.up' ) . popup ( ) ;
$ ( this . $el ) . find ( '.dropdown' ) . dropdown ( ) ;
this . setCheckboxes ( ) ;
Vue . nextTick ( ( ) => {
this . $refs . search . focus ( ) ;
} ) ;
} ,
methods : {
changeTab ( t ) {
this . tab = t ;
this . updateHistory ( ) ;
} ,
setCheckboxes ( ) {
switch ( this . archivedFilter ) {
case 'unarchived' :
$ ( '#archivedFilterCheckbox' ) . checkbox ( 'set unchecked' ) ;
break ;
case 'archived' :
$ ( '#archivedFilterCheckbox' ) . checkbox ( 'set checked' ) ;
break ;
case 'both' :
$ ( '#archivedFilterCheckbox' ) . checkbox ( 'set indeterminate' ) ;
break ;
default :
this . archivedFilter = 'unarchived' ;
$ ( '#archivedFilterCheckbox' ) . checkbox ( 'set unchecked' ) ;
break ;
}
switch ( this . privateFilter ) {
case 'public' :
$ ( '#privateFilterCheckbox' ) . checkbox ( 'set unchecked' ) ;
break ;
case 'private' :
$ ( '#privateFilterCheckbox' ) . checkbox ( 'set checked' ) ;
break ;
case 'both' :
$ ( '#privateFilterCheckbox' ) . checkbox ( 'set indeterminate' ) ;
break ;
default :
this . privateFilter = 'both' ;
$ ( '#privateFilterCheckbox' ) . checkbox ( 'set indeterminate' ) ;
break ;
}
} ,
changeReposFilter ( filter ) {
this . reposFilter = filter ;
this . repos = [ ] ;
this . page = 1 ;
Vue . set ( this . counts , ` ${ filter } : ${ this . archivedFilter } : ${ this . privateFilter } ` , 0 ) ;
this . searchRepos ( ) ;
} ,
updateHistory ( ) {
const params = new URLSearchParams ( window . location . search ) ;
if ( this . tab === 'repos' ) {
params . delete ( 'repo-search-tab' ) ;
} else {
params . set ( 'repo-search-tab' , this . tab ) ;
}
if ( this . reposFilter === 'all' ) {
params . delete ( 'repo-search-filter' ) ;
} else {
params . set ( 'repo-search-filter' , this . reposFilter ) ;
}
if ( this . privateFilter === 'both' ) {
params . delete ( 'repo-search-private' ) ;
} else {
params . set ( 'repo-search-private' , this . privateFilter ) ;
}
if ( this . archivedFilter === 'unarchived' ) {
params . delete ( 'repo-search-archived' ) ;
} else {
params . set ( 'repo-search-archived' , this . archivedFilter ) ;
}
if ( this . searchQuery === '' ) {
params . delete ( 'repo-search-query' ) ;
} else {
params . set ( 'repo-search-query' , this . searchQuery ) ;
}
if ( this . page === 1 ) {
params . delete ( 'repo-search-page' ) ;
} else {
params . set ( 'repo-search-page' , ` ${ this . page } ` ) ;
}
const queryString = params . toString ( ) ;
if ( queryString ) {
window . history . replaceState ( { } , '' , ` ? ${ queryString } ` ) ;
} else {
window . history . replaceState ( { } , '' , window . location . pathname ) ;
}
} ,
toggleArchivedFilter ( ) {
switch ( this . archivedFilter ) {
case 'both' :
this . archivedFilter = 'unarchived' ;
break ;
case 'unarchived' :
this . archivedFilter = 'archived' ;
break ;
case 'archived' :
this . archivedFilter = 'both' ;
break ;
default :
this . archivedFilter = 'unarchived' ;
break ;
}
this . page = 1 ;
this . repos = [ ] ;
this . setCheckboxes ( ) ;
Vue . set ( this . counts , ` ${ this . reposFilter } : ${ this . archivedFilter } : ${ this . privateFilter } ` , 0 ) ;
this . searchRepos ( ) ;
} ,
togglePrivateFilter ( ) {
switch ( this . privateFilter ) {
case 'both' :
this . privateFilter = 'public' ;
break ;
case 'public' :
this . privateFilter = 'private' ;
break ;
case 'private' :
this . privateFilter = 'both' ;
break ;
default :
this . privateFilter = 'both' ;
break ;
}
this . page = 1 ;
this . repos = [ ] ;
this . setCheckboxes ( ) ;
Vue . set ( this . counts , ` ${ this . reposFilter } : ${ this . archivedFilter } : ${ this . privateFilter } ` , 0 ) ;
this . searchRepos ( ) ;
} ,
changePage ( page ) {
this . page = page ;
if ( this . page > this . finalPage ) {
this . page = this . finalPage ;
}
if ( this . page < 1 ) {
this . page = 1 ;
}
this . repos = [ ] ;
Vue . set ( this . counts , ` ${ this . reposFilter } : ${ this . archivedFilter } : ${ this . privateFilter } ` , 0 ) ;
this . searchRepos ( ) ;
} ,
searchRepos ( ) {
this . isLoading = true ;
if ( ! this . reposTotalCount ) {
const totalCountSearchURL = ` ${ this . suburl } /api/v1/repos/search?sort=updated&order=desc&uid= ${ this . uid } &team_id= ${ this . teamId } &q=&page=1&mode= ` ;
$ . getJSON ( totalCountSearchURL , ( _result , _textStatus , request ) => {
this . reposTotalCount = request . getResponseHeader ( 'X-Total-Count' ) ;
} ) ;
}
const searchedMode = this . repoTypes [ this . reposFilter ] . searchMode ;
const searchedURL = this . searchURL ;
const searchedQuery = this . searchQuery ;
$ . getJSON ( searchedURL , ( result , _textStatus , request ) => {
if ( searchedURL === this . searchURL ) {
this . repos = result . data ;
const count = request . getResponseHeader ( 'X-Total-Count' ) ;
if ( searchedQuery === '' && searchedMode === '' && this . archivedFilter === 'both' ) {
this . reposTotalCount = count ;
}
Vue . set ( this . counts , ` ${ this . reposFilter } : ${ this . archivedFilter } : ${ this . privateFilter } ` , count ) ;
this . finalPage = Math . ceil ( count / this . searchLimit ) ;
this . updateHistory ( ) ;
}
} ) . always ( ( ) => {
if ( searchedURL === this . searchURL ) {
this . isLoading = false ;
}
} ) ;
} ,
repoIcon ( repo ) {
if ( repo . fork ) {
return 'octicon-repo-forked' ;
} else if ( repo . mirror ) {
return 'octicon-mirror' ;
} else if ( repo . template ) {
return ` octicon-repo-template ` ;
} else if ( repo . private ) {
return 'octicon-lock' ;
} else if ( repo . internal ) {
return 'octicon-repo' ;
}
return 'octicon-repo' ;
}
}
} ) ;
}
function initCtrlEnterSubmit ( ) {
$ ( '.js-quick-submit' ) . on ( 'keydown' , function ( e ) {
if ( ( ( e . ctrlKey && ! e . altKey ) || e . metaKey ) && ( e . keyCode === 13 || e . keyCode === 10 ) ) {
@ -3470,31 +3107,6 @@ function initCtrlEnterSubmit() {
} ) ;
}
function initVueApp ( ) {
const el = document . getElementById ( 'app' ) ;
if ( ! el ) {
return ;
}
initVueComponents ( ) ;
new Vue ( {
el ,
delimiters : [ '${' , '}' ] ,
components : {
ActivityTopAuthors ,
} ,
data : ( ) => {
return {
searchLimit : Number ( ( document . querySelector ( 'meta[name=_search_limit]' ) || { } ) . content ) ,
suburl : AppSubUrl ,
uid : Number ( ( document . querySelector ( 'meta[name=_context_uid]' ) || { } ) . content ) ,
activityTopAuthors : window . ActivityTopAuthors || [ ] ,
} ;
} ,
} ) ;
}
function initIssueTimetracking ( ) {
$ ( document ) . on ( 'click' , '.issue-add-time' , ( ) => {
$ ( '.issue-start-time-modal' ) . modal ( {
@ -3537,163 +3149,6 @@ function initBranchOrTagDropdown(selector) {
} ) ;
}
function initFilterBranchTagDropdown ( selector ) {
$ ( selector ) . each ( function ( ) {
const $dropdown = $ ( this ) ;
const $data = $dropdown . find ( '.data' ) ;
const data = {
items : [ ] ,
mode : $data . data ( 'mode' ) ,
searchTerm : '' ,
noResults : '' ,
canCreateBranch : false ,
menuVisible : false ,
createTag : false ,
active : 0
} ;
$data . find ( '.item' ) . each ( function ( ) {
data . items . push ( {
name : $ ( this ) . text ( ) ,
url : $ ( this ) . data ( 'url' ) ,
branch : $ ( this ) . hasClass ( 'branch' ) ,
tag : $ ( this ) . hasClass ( 'tag' ) ,
selected : $ ( this ) . hasClass ( 'selected' )
} ) ;
} ) ;
$data . remove ( ) ;
new Vue ( {
el : this ,
delimiters : [ '${' , '}' ] ,
data ,
computed : {
filteredItems ( ) {
const items = this . items . filter ( ( item ) => {
return ( ( this . mode === 'branches' && item . branch ) || ( this . mode === 'tags' && item . tag ) ) &&
( ! this . searchTerm || item . name . toLowerCase ( ) . includes ( this . searchTerm . toLowerCase ( ) ) ) ;
} ) ;
// no idea how to fix this so linting rule is disabled instead
this . active = ( items . length === 0 && this . showCreateNewBranch ? 0 : - 1 ) ; // eslint-disable-line vue/no-side-effects-in-computed-properties
return items ;
} ,
showNoResults ( ) {
return this . filteredItems . length === 0 && ! this . showCreateNewBranch ;
} ,
showCreateNewBranch ( ) {
if ( ! this . canCreateBranch || ! this . searchTerm ) {
return false ;
}
return this . items . filter ( ( item ) => item . name . toLowerCase ( ) === this . searchTerm . toLowerCase ( ) ) . length === 0 ;
}
} ,
watch : {
menuVisible ( visible ) {
if ( visible ) {
this . focusSearchField ( ) ;
}
}
} ,
beforeMount ( ) {
this . noResults = this . $el . getAttribute ( 'data-no-results' ) ;
this . canCreateBranch = this . $el . getAttribute ( 'data-can-create-branch' ) === 'true' ;
document . body . addEventListener ( 'click' , ( event ) => {
if ( this . $el . contains ( event . target ) ) return ;
if ( this . menuVisible ) {
Vue . set ( this , 'menuVisible' , false ) ;
}
} ) ;
} ,
methods : {
selectItem ( item ) {
const prev = this . getSelected ( ) ;
if ( prev !== null ) {
prev . selected = false ;
}
item . selected = true ;
window . location . href = item . url ;
} ,
createNewBranch ( ) {
if ( ! this . showCreateNewBranch ) return ;
$ ( this . $refs . newBranchForm ) . trigger ( 'submit' ) ;
} ,
focusSearchField ( ) {
Vue . nextTick ( ( ) => {
this . $refs . searchField . focus ( ) ;
} ) ;
} ,
getSelected ( ) {
for ( let i = 0 , j = this . items . length ; i < j ; ++ i ) {
if ( this . items [ i ] . selected ) return this . items [ i ] ;
}
return null ;
} ,
getSelectedIndexInFiltered ( ) {
for ( let i = 0 , j = this . filteredItems . length ; i < j ; ++ i ) {
if ( this . filteredItems [ i ] . selected ) return i ;
}
return - 1 ;
} ,
scrollToActive ( ) {
let el = this . $refs [ ` listItem ${ this . active } ` ] ;
if ( ! el || ! el . length ) return ;
if ( Array . isArray ( el ) ) {
el = el [ 0 ] ;
}
const cont = this . $refs . scrollContainer ;
if ( el . offsetTop < cont . scrollTop ) {
cont . scrollTop = el . offsetTop ;
} else if ( el . offsetTop + el . clientHeight > cont . scrollTop + cont . clientHeight ) {
cont . scrollTop = el . offsetTop + el . clientHeight - cont . clientHeight ;
}
} ,
keydown ( event ) {
if ( event . keyCode === 40 ) { // arrow down
event . preventDefault ( ) ;
if ( this . active === - 1 ) {
this . active = this . getSelectedIndexInFiltered ( ) ;
}
if ( this . active + ( this . showCreateNewBranch ? 0 : 1 ) >= this . filteredItems . length ) {
return ;
}
this . active ++ ;
this . scrollToActive ( ) ;
} else if ( event . keyCode === 38 ) { // arrow up
event . preventDefault ( ) ;
if ( this . active === - 1 ) {
this . active = this . getSelectedIndexInFiltered ( ) ;
}
if ( this . active <= 0 ) {
return ;
}
this . active -- ;
this . scrollToActive ( ) ;
} else if ( event . keyCode === 13 ) { // enter
event . preventDefault ( ) ;
if ( this . active >= this . filteredItems . length ) {
this . createNewBranch ( ) ;
} else if ( this . active >= 0 ) {
this . selectItem ( this . filteredItems [ this . active ] ) ;
}
} else if ( event . keyCode === 27 ) { // escape
event . preventDefault ( ) ;
this . menuVisible = false ;
}
}
}
} ) ;
} ) ;
}
$ ( '.commit-button' ) . on ( 'click' , function ( e ) {
e . preventDefault ( ) ;