126126 . issue-row : hover { background : color-mix ( in srgb, var ( --accent ) 5% , transparent) ; }
127127 . issue-row . chosen { background : color-mix ( in srgb, var ( --accent ) 10% , transparent) ; }
128128 . issue-row code { font-weight : 500 ; }
129+ . gaps ul { margin : 4px 0 12px ; padding-left : 18px ; }
129130 . issue-row . summary { overflow : hidden; text-overflow : ellipsis; white-space : nowrap; }
130131 . issue-row . reach { grid-column : 3 / -1 ; display : flex; gap : 4px ; overflow : hidden; white-space : nowrap; }
131132 . rider-mark { padding : 0 6px ; border : 1px dashed var ( --warn ) ; border-radius : 999px ; color : var ( --warn ) ; font-size : 11px ; white-space : nowrap; }
@@ -284,8 +285,9 @@ <h2 x-text="t('services')"></h2>
284285 </ td >
285286 < td >
286287 < template x-for ="key in row.jira_keys " :key ="key.key ">
287- < div >
288+ < div class =" row-key " >
288289 < a :href ="key.url " x-text ="key.key "> </ a >
290+ < span class ="muted via " :title ="t('viaTitle') " x-text ="viaText(key) "> </ span >
289291 < span x-show ="issue(key.key) " :class ="statusClass(issue(key.key)) " x-text ="issue(key.key) && issue(key.key).status "> </ span >
290292 < span x-text ="issue(key.key) ? issue(key.key).summary : '' "> </ span >
291293 </ div >
@@ -354,7 +356,7 @@ <h2><span x-text="t('chooseIssues')"></span>
354356 < label class ="issue-row " :class ="{ chosen: chosen.includes(key) } ">
355357 < input type ="checkbox " :checked ="chosen.includes(key) " @change ="toggleIssue(key) ">
356358 < span class ="dot " :class ="'dot-' + (issue(key).status_category || 'new') " :title ="issue(key).status "> </ span >
357- < code x-text ="key "> </ code >
359+ < code :title =" relationsText(key) " x-text ="key "> </ code >
358360 < span class ="summary " :title ="issue(key).summary " x-text ="issue(key).summary "> </ span >
359361 < span class ="rider-mark " :title ="t('shipsAnywayTitle') " x-text ="t('shipsAnyway') "
360362 :style ="{ visibility: shipsAnyway(key) ? 'visible' : 'hidden' } "> </ span >
@@ -374,6 +376,12 @@ <h2 x-text="t('release')"></h2>
374376 < p x-show ="!pickable.length " class ="muted " x-text ="t('noCandidates') "> </ p >
375377 < p x-show ="pickable.length && !picked.length " class ="muted " x-text ="t('pickToFill') "> </ p >
376378 < p x-show ="copyFailed " class ="warn " x-text ="t('copyFailed') "> </ p >
379+ < div class ="gaps " x-show ="picked.length && gaps.length ">
380+ < p class ="warn " x-text ="t('gapsTitle') "> </ p >
381+ < ul >
382+ < template x-for ="gap in gaps " :key ="gap "> < li x-text ="gap "> </ li > </ template >
383+ </ ul >
384+ </ div >
377385
378386 < div class ="outputs " x-show ="picked.length ">
379387 < div class ="tabs ">
@@ -394,6 +402,7 @@ <h2 x-text="t('release')"></h2>
394402 < span x-show ="task.issue " :class ="statusClass(task.issue) " x-text ="task.issue && task.issue.status "> </ span >
395403 < span x-text ="task.issue ? task.issue.summary : '' "> </ span >
396404 < span x-show ="isMissing(task.key) " class ="warn " x-text ="t('notInJira') "> </ span >
405+ < span x-show ="task.sources.length " class ="muted sources " x-text ="t('taskFrom', { items: task.sources.join(', ') }) "> </ span >
397406 </ li >
398407 </ template >
399408 </ ul >
@@ -551,6 +560,15 @@ <h2 x-text="t('release')"></h2>
551560 planAlso : "also ships" ,
552561 planNoTag : "{keys}: no tag yet." ,
553562 planElsewhere : "{items}, nothing to pick" ,
563+ groupRelated : "Related" ,
564+ viaNamed : "named" ,
565+ viaLinked : "linked" ,
566+ viaTitle : "named: the merge request or commit mentions the key; linked: the Jira issue links to this change" ,
567+ taskFrom : "from {items}" ,
568+ gapsTitle : "Left behind by these picks:" ,
569+ gapNeeds : "needs {tag}" ,
570+ relationParent : "has parent" ,
571+ relationSubtask : "has subtask" ,
554572 } ,
555573 ru : {
556574 language : "Язык" ,
@@ -651,6 +669,15 @@ <h2 x-text="t('release')"></h2>
651669 planAlso : "также войдут" ,
652670 planNoTag : "{keys}: тега ещё нет." ,
653671 planElsewhere : "{items}, выбирать нечего" ,
672+ groupRelated : "Связанные" ,
673+ viaNamed : "в тексте" ,
674+ viaLinked : "по ссылке" ,
675+ viaTitle : "в тексте: ключ упомянут в MR или коммите; по ссылке: задача в Jira ссылается на это изменение" ,
676+ taskFrom : "из {items}" ,
677+ gapsTitle : "Не войдёт с этими тегами:" ,
678+ gapNeeds : "нужен {tag}" ,
679+ relationParent : "родитель" ,
680+ relationSubtask : "подзадача" ,
654681 } ,
655682 } ;
656683
@@ -881,13 +908,64 @@ <h2 x-text="t('release')"></h2>
881908 } ,
882909 get tasks ( ) {
883910 const tasks = new Map ( ) ;
884- for ( const { candidate } of this . picked ) {
911+ for ( const { service, candidate } of this . picked ) {
912+ const shipped = service . rows . slice ( this . tagRow ( service , candidate . tag . name ) ) . filter ( ( row ) => row . in_scope ) ;
885913 for ( const key of candidate . jira_keys ) {
886- if ( ! tasks . has ( key . key ) ) tasks . set ( key . key , { key : key . key , url : key . url , issue : this . issue ( key . key ) } ) ;
914+ if ( ! tasks . has ( key . key ) ) tasks . set ( key . key , { key : key . key , url : key . url , issue : this . issue ( key . key ) , sources : [ ] } ) ;
915+ tasks . get ( key . key ) . sources . push (
916+ ...shipped
917+ . filter ( ( row ) => row . jira_keys . some ( ( item ) => item . key === key . key ) )
918+ . map ( ( row ) => `${ this . shortName ( service . project ) } ${ this . changeLabel ( row ) } ` ) ,
919+ ) ;
887920 }
888921 }
889922 return [ ...tasks . values ( ) ] ;
890923 } ,
924+ get gaps ( ) {
925+ const starts = new Map ( this . picked . map ( ( { service, candidate } ) => [ service . project , this . tagRow ( service , candidate . tag . name ) ] ) ) ;
926+ const gaps = new Set ( ) ;
927+ const check = ( source , relation , key ) => {
928+ const via = relation ? ` ${ this . relationLabel ( relation ) } ${ key } ` : "" ;
929+ const reach = this . reach [ key ] || [ ] ;
930+ for ( const item of reach ) {
931+ const reason = this . gapReason ( item , starts ) ;
932+ if ( reason ) gaps . add ( `${ source } ${ via } → ${ this . shortName ( item . project ) } : ${ reason } ` ) ;
933+ }
934+ const issue = this . issue ( key ) ;
935+ if ( relation && ! reach . length && issue && issue . status_category !== "done" ) gaps . add ( `${ source } ${ via } : ${ issue . status } ` ) ;
936+ } ;
937+ for ( const task of this . tasks ) {
938+ check ( task . key , null , task . key ) ;
939+ for ( const relation of task . issue ? task . issue . relations : [ ] ) check ( task . key , relation , relation . key ) ;
940+ }
941+ return [ ...gaps ] ;
942+ } ,
943+ gapReason ( item , starts ) {
944+ if ( item . state === "tag" || item . state === "untagged" ) {
945+ const start = starts . get ( item . project ) ;
946+ if ( start !== undefined && start <= item . index ) return "" ;
947+ return item . state === "tag" ? this . t ( "gapNeeds" , { tag : item . tag } ) : this . t ( "reachUntagged" ) ;
948+ }
949+ return [ "production" , "older" , "closed" ] . includes ( item . state ) ? "" : this . reachLabel ( item ) ;
950+ } ,
951+ tagRow ( service , tag ) {
952+ return service . rows . findIndex ( ( row ) => row . tags . some ( ( item ) => item . name === tag ) ) ;
953+ } ,
954+ changeLabel ( row ) {
955+ return row . merge_requests . length ? row . merge_requests . map ( ( item ) => "!" + item . iid ) . join ( " " ) : row . commits [ 0 ] . short_sha ;
956+ } ,
957+ viaText ( key ) {
958+ return key . via . map ( ( source ) => this . t ( source === "named" ? "viaNamed" : "viaLinked" ) ) . join ( ", " ) ;
959+ } ,
960+ relationLabel ( relation ) {
961+ if ( relation . kind === "parent" ) return this . t ( "relationParent" ) ;
962+ if ( relation . kind === "subtask" ) return this . t ( "relationSubtask" ) ;
963+ return relation . label ;
964+ } ,
965+ relationsText ( key ) {
966+ const issue = this . issue ( key ) ;
967+ return issue ? issue . relations . map ( ( relation ) => `${ this . relationLabel ( relation ) } ${ relation . key } ` ) . join ( "\n" ) : "" ;
968+ } ,
891969 get taskKeys ( ) {
892970 return this . tasks . map ( ( task ) => task . key ) . join ( "\n" ) ;
893971 } ,
@@ -938,9 +1016,11 @@ <h2 x-text="t('release')"></h2>
9381016 ( ! text || `${ key } ${ this . issue ( key ) . summary } ` . toLowerCase ( ) . includes ( text ) ) ,
9391017 ) ;
9401018 const done = ( key ) => this . issue ( key ) . status_category === "done" ;
1019+ const related = ( key ) => this . issue ( key ) . via_relation ;
9411020 return [
942- { name : "groupDone" , keys : keys . filter ( done ) } ,
943- { name : "groupNotDone" , keys : keys . filter ( ( key ) => ! done ( key ) ) } ,
1021+ { name : "groupDone" , keys : keys . filter ( ( key ) => ! related ( key ) && done ( key ) ) } ,
1022+ { name : "groupNotDone" , keys : keys . filter ( ( key ) => ! related ( key ) && ! done ( key ) ) } ,
1023+ { name : "groupRelated" , keys : keys . filter ( related ) } ,
9441024 ] . filter ( ( group ) => group . keys . length ) ;
9451025 } ,
9461026 get plan ( ) {
@@ -1023,7 +1103,7 @@ <h2 x-text="t('release')"></h2>
10231103 } ,
10241104 untickMissed ( service ) {
10251105 const tag = this . picks [ service . project ] ;
1026- const picked = tag ? service . rows . findIndex ( ( row ) => row . tags . some ( ( item ) => item . name === tag ) ) : - 1 ;
1106+ const picked = tag ? this . tagRow ( service , tag ) : - 1 ;
10271107 const missed = this . chosen . filter ( ( key ) =>
10281108 this . reach [ key ] . some ( ( item ) => item . project === service . project && item . state === "tag" && ( picked === - 1 || picked > item . index ) ) ) ;
10291109 if ( ! missed . length ) return ;
0 commit comments