Load issue/PR context popup data only when needed (#15955)
* Load issue/PR context popup data only when needed * Add SVG icon Vue component * Remove unneeded check
This commit is contained in:
		
							parent
							
								
									3dba75fb97
								
							
						
					
					
						commit
						d26551bd0c
					
				
					 3 changed files with 154 additions and 57 deletions
				
			
		
							
								
								
									
										112
									
								
								web_src/js/components/ContextPopup.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								web_src/js/components/ContextPopup.vue
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,112 @@ | ||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <div v-if="loading" class="ui active centered inline loader"/> | ||||||
|  |     <div v-if="!loading && issue !== null"> | ||||||
|  |       <p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p> | ||||||
|  |       <p><svg-icon :name="icon" :class="[color]" /> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p> | ||||||
|  |       <p>{{ body }}</p> | ||||||
|  |       <div> | ||||||
|  |         <div | ||||||
|  |           v-for="label in labels" | ||||||
|  |           :key="label.name" | ||||||
|  |           class="ui label" | ||||||
|  |           :style="{ color: label.textColor, backgroundColor: label.color }" | ||||||
|  |         > | ||||||
|  |           {{ label.name }} | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import {SvgIcon} from '../svg.js'; | ||||||
|  | 
 | ||||||
|  | const {AppSubUrl} = window.config; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: 'ContextPopup', | ||||||
|  | 
 | ||||||
|  |   components: { | ||||||
|  |     SvgIcon, | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   data: () => ({ | ||||||
|  |     loading: false, | ||||||
|  |     issue: null | ||||||
|  |   }), | ||||||
|  | 
 | ||||||
|  |   computed: { | ||||||
|  |     createdAt() { | ||||||
|  |       return new Date(this.issue.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'}); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     body() { | ||||||
|  |       const body = this.issue.body.replace(/\n+/g, ' '); | ||||||
|  |       if (body.length > 85) { | ||||||
|  |         return `${body.substring(0, 85)}…`; | ||||||
|  |       } | ||||||
|  |       return body; | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     icon() { | ||||||
|  |       if (this.issue.pull_request !== null) { | ||||||
|  |         if (this.issue.state === 'open') { | ||||||
|  |           return 'octicon-git-pull-request'; // Open PR | ||||||
|  |         } else if (this.issue.pull_request.merged === true) { | ||||||
|  |           return 'octicon-git-merge'; // Merged PR | ||||||
|  |         } | ||||||
|  |         return 'octicon-git-pull-request'; // Closed PR | ||||||
|  |       } else if (this.issue.state === 'open') { | ||||||
|  |         return 'octicon-issue-opened'; // Open Issue | ||||||
|  |       } | ||||||
|  |       return 'octicon-issue-closed'; // Closed Issue | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     color() { | ||||||
|  |       if (this.issue.state === 'open') { | ||||||
|  |         return 'green'; | ||||||
|  |       } else if (this.issue.pull_request !== null && this.issue.pull_request.merged === true) { | ||||||
|  |         return 'purple'; | ||||||
|  |       } | ||||||
|  |       return 'red'; | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     labels() { | ||||||
|  |       return this.issue.labels.map((label) => { | ||||||
|  |         const red = parseInt(label.color.substring(0, 2), 16); | ||||||
|  |         const green = parseInt(label.color.substring(2, 4), 16); | ||||||
|  |         const blue = parseInt(label.color.substring(4, 6), 16); | ||||||
|  |         let color = '#ffffff'; | ||||||
|  |         if ((red * 0.299 + green * 0.587 + blue * 0.114) > 125) { | ||||||
|  |           color = '#000000'; | ||||||
|  |         } | ||||||
|  |         return {name: label.name, color: `#${label.color}`, textColor: color}; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   mounted() { | ||||||
|  |     this.$root.$on('load-context-popup', (data, callback) => { | ||||||
|  |       if (!this.loading && this.issue === null) { | ||||||
|  |         this.load(data, callback); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   methods: { | ||||||
|  |     load(data, callback) { | ||||||
|  |       this.loading = true; | ||||||
|  |       $.get(`${AppSubUrl}/api/v1/repos/${data.owner}/${data.repo}/issues/${data.index}`, (issue) => { | ||||||
|  |         this.issue = issue; | ||||||
|  |         this.loading = false; | ||||||
|  |         this.$nextTick(() => { | ||||||
|  |           if (callback) { | ||||||
|  |             callback(); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| import {htmlEscape} from 'escape-goat'; | import Vue from 'vue'; | ||||||
| import {svg} from '../svg.js'; |  | ||||||
| 
 | 
 | ||||||
| const {AppSubUrl} = window.config; | import ContextPopup from '../components/ContextPopup.vue'; | ||||||
| 
 | 
 | ||||||
| export default function initContextPopups() { | export default function initContextPopups() { | ||||||
|   const refIssues = $('.ref-issue'); |   const refIssues = $('.ref-issue'); | ||||||
|  | @ -9,68 +8,36 @@ export default function initContextPopups() { | ||||||
| 
 | 
 | ||||||
|   refIssues.each(function () { |   refIssues.each(function () { | ||||||
|     const [index, _issues, repo, owner] = $(this).attr('href').replace(/[#?].*$/, '').split('/').reverse(); |     const [index, _issues, repo, owner] = $(this).attr('href').replace(/[#?].*$/, '').split('/').reverse(); | ||||||
|     issuePopup(owner, repo, index, $(this)); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| function issuePopup(owner, repo, index, $element) { |     const el = document.createElement('div'); | ||||||
|   $.get(`${AppSubUrl}/api/v1/repos/${owner}/${repo}/issues/${index}`, (issue) => { |     el.className = 'ui custom popup hidden'; | ||||||
|     const createdAt = new Date(issue.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'}); |     el.innerHTML = '<div></div>'; | ||||||
|  |     this.parentNode.insertBefore(el, this.nextSibling); | ||||||
| 
 | 
 | ||||||
|     let body = issue.body.replace(/\n+/g, ' '); |     const View = Vue.extend({ | ||||||
|     if (body.length > 85) { |       render: (createElement) => createElement(ContextPopup), | ||||||
|       body = `${body.substring(0, 85)}...`; |     }); | ||||||
|  | 
 | ||||||
|  |     const view = new View(); | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       view.$mount(el.firstChild); | ||||||
|  |     } catch (err) { | ||||||
|  |       console.error(err); | ||||||
|  |       el.textContent = 'ContextPopup failed to load'; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let labels = ''; |     $(this).popup({ | ||||||
|     for (let i = 0; i < issue.labels.length; i++) { |  | ||||||
|       const label = issue.labels[i]; |  | ||||||
|       const red = parseInt(label.color.substring(0, 2), 16); |  | ||||||
|       const green = parseInt(label.color.substring(2, 4), 16); |  | ||||||
|       const blue = parseInt(label.color.substring(4, 6), 16); |  | ||||||
|       let color = '#ffffff'; |  | ||||||
|       if ((red * 0.299 + green * 0.587 + blue * 0.114) > 125) { |  | ||||||
|         color = '#000000'; |  | ||||||
|       } |  | ||||||
|       labels += `<div class="ui label" style="color: ${color}; background-color:#${label.color};">${htmlEscape(label.name)}</div>`; |  | ||||||
|     } |  | ||||||
|     if (labels.length > 0) { |  | ||||||
|       labels = `<p>${labels}</p>`; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     let octicon, color; |  | ||||||
|     if (issue.pull_request !== null) { |  | ||||||
|       if (issue.state === 'open') { |  | ||||||
|         color = 'green'; |  | ||||||
|         octicon = 'octicon-git-pull-request'; // Open PR
 |  | ||||||
|       } else if (issue.pull_request.merged === true) { |  | ||||||
|         color = 'purple'; |  | ||||||
|         octicon = 'octicon-git-merge'; // Merged PR
 |  | ||||||
|       } else { |  | ||||||
|         color = 'red'; |  | ||||||
|         octicon = 'octicon-git-pull-request'; // Closed PR
 |  | ||||||
|       } |  | ||||||
|     } else if (issue.state === 'open') { |  | ||||||
|       color = 'green'; |  | ||||||
|       octicon = 'octicon-issue-opened'; // Open Issue
 |  | ||||||
|     } else { |  | ||||||
|       color = 'red'; |  | ||||||
|       octicon = 'octicon-issue-closed'; // Closed Issue
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     $element.popup({ |  | ||||||
|       variation: 'wide', |       variation: 'wide', | ||||||
|       delay: { |       delay: { | ||||||
|         show: 250 |         show: 250 | ||||||
|       }, |       }, | ||||||
|       html: ` |       onShow: () => { | ||||||
| <div> |         view.$emit('load-context-popup', {owner, repo, index}, () => { | ||||||
|   <p><small>${htmlEscape(issue.repository.full_name)} on ${createdAt}</small></p> |           $(this).popup('reposition'); | ||||||
|   <p><span class="${color}">${svg(octicon)}</span> <strong>${htmlEscape(issue.title)}</strong> #${index}</p> |         }); | ||||||
|   <p>${htmlEscape(body)}</p> |       }, | ||||||
|   ${labels} |       popup: $(el), | ||||||
| </div> |  | ||||||
| ` |  | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -14,6 +14,8 @@ import octiconRepo from '../../public/img/svg/octicon-repo.svg'; | ||||||
| import octiconRepoForked from '../../public/img/svg/octicon-repo-forked.svg'; | import octiconRepoForked from '../../public/img/svg/octicon-repo-forked.svg'; | ||||||
| import octiconRepoTemplate from '../../public/img/svg/octicon-repo-template.svg'; | import octiconRepoTemplate from '../../public/img/svg/octicon-repo-template.svg'; | ||||||
| 
 | 
 | ||||||
|  | import Vue from 'vue'; | ||||||
|  | 
 | ||||||
| export const svgs = { | export const svgs = { | ||||||
|   'octicon-chevron-down': octiconChevronDown, |   'octicon-chevron-down': octiconChevronDown, | ||||||
|   'octicon-chevron-right': octiconChevronRight, |   'octicon-chevron-right': octiconChevronRight, | ||||||
|  | @ -47,3 +49,19 @@ export function svg(name, size = 16, className = '') { | ||||||
|   if (className) svgNode.classList.add(...className.split(/\s+/)); |   if (className) svgNode.classList.add(...className.split(/\s+/)); | ||||||
|   return serializer.serializeToString(svgNode); |   return serializer.serializeToString(svgNode); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export const SvgIcon = Vue.component('SvgIcon', { | ||||||
|  |   props: { | ||||||
|  |     name: {type: String, required: true}, | ||||||
|  |     size: {type: Number, default: 16}, | ||||||
|  |     className: {type: String, default: ''}, | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   computed: { | ||||||
|  |     svg() { | ||||||
|  |       return svg(this.name, this.size, this.className); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   template: `<span v-html="svg" />` | ||||||
|  | }); | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue