mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	Migrate vue components to setup (#32329)
Migrated a handful Vue components to the `setup` syntax using composition api as it has better Typescript support and is becoming the new default in the Vue ecosystem. - [x] ActionRunStatus.vue - [x] ActivityHeatmap.vue - [x] ContextPopup.vue - [x] DiffFileList.vue - [x] DiffFileTree.vue - [x] DiffFileTreeItem.vue - [x] PullRequestMergeForm.vue - [x] RepoActivityTopAuthors.vue - [x] RepoCodeFrequency.vue - [x] RepoRecentCommits.vue - [x] ScopedAccessTokenSelector.vue Left some larger components untouched for now to not go to crazy in this single PR: - [ ] DiffCommitSelector.vue - [ ] RepoActionView.vue - [ ] RepoContributors.vue - [ ] DashboardRepoList.vue - [ ] RepoBranchTagSelector.vue
This commit is contained in:
		| @@ -2,31 +2,21 @@ | ||||
|     Please also update the template file above if this vue is modified. | ||||
|     action status accepted: success, skipped, waiting, blocked, running, failure, cancelled, unknown | ||||
| --> | ||||
| <script lang="ts"> | ||||
| <script lang="ts" setup> | ||||
| import {SvgIcon} from '../svg.ts'; | ||||
|  | ||||
| export default { | ||||
|   components: {SvgIcon}, | ||||
|   props: { | ||||
|     status: { | ||||
|       type: String, | ||||
|       required: true, | ||||
|     }, | ||||
|     size: { | ||||
|       type: Number, | ||||
|       default: 16, | ||||
|     }, | ||||
|     className: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     localeStatus: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| withDefaults(defineProps<{ | ||||
|   status: '', | ||||
|   size?: number, | ||||
|   className?: string, | ||||
|   localeStatus?: string, | ||||
| }>(), { | ||||
|   size: 16, | ||||
|   className: undefined, | ||||
|   localeStatus: undefined, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <span class="tw-flex tw-items-center" :data-tooltip-content="localeStatus" v-if="status"> | ||||
|     <SvgIcon name="octicon-check-circle-fill" class="text green" :size="size" :class-name="className" v-if="status === 'success'"/> | ||||
|   | ||||
| @@ -1,21 +1,20 @@ | ||||
| <script lang="ts"> | ||||
| <script lang="ts" setup> | ||||
| // TODO: Switch to upstream after https://github.com/razorness/vue3-calendar-heatmap/pull/34 is merged | ||||
| import {CalendarHeatmap} from '@silverwind/vue3-calendar-heatmap'; | ||||
| import {onMounted, ref} from 'vue'; | ||||
| import type {Value as HeatmapValue, Locale as HeatmapLocale} from '@silverwind/vue3-calendar-heatmap'; | ||||
|  | ||||
| export default { | ||||
|   components: {CalendarHeatmap}, | ||||
|   props: { | ||||
|     values: { | ||||
|       type: Array, | ||||
|       default: () => [], | ||||
|     }, | ||||
| defineProps<{ | ||||
|   values?: HeatmapValue[]; | ||||
|   locale: { | ||||
|       type: Object, | ||||
|       default: () => {}, | ||||
|     }, | ||||
|   }, | ||||
|   data: () => ({ | ||||
|     colorRange: [ | ||||
|     textTotalContributions: string; | ||||
|     heatMapLocale: Partial<HeatmapLocale>; | ||||
|     noDataText: string; | ||||
|     tooltipUnit: string; | ||||
|   }; | ||||
| }>(); | ||||
|  | ||||
| const colorRange = [ | ||||
|   'var(--color-secondary-alpha-60)', | ||||
|   'var(--color-secondary-alpha-60)', | ||||
|   'var(--color-primary-light-4)', | ||||
| @@ -23,22 +22,23 @@ export default { | ||||
|   'var(--color-primary)', | ||||
|   'var(--color-primary-dark-2)', | ||||
|   'var(--color-primary-dark-4)', | ||||
|     ], | ||||
|     endDate: new Date(), | ||||
|   }), | ||||
|   mounted() { | ||||
| ]; | ||||
|  | ||||
| const endDate = ref(new Date()); | ||||
|  | ||||
| onMounted(() => { | ||||
|   // work around issue with first legend color being rendered twice and legend cut off | ||||
|     const legend = document.querySelector('.vch__external-legend-wrapper'); | ||||
|   const legend = document.querySelector<HTMLElement>('.vch__external-legend-wrapper'); | ||||
|   legend.setAttribute('viewBox', '12 0 80 10'); | ||||
|   legend.style.marginRight = '-12px'; | ||||
|   }, | ||||
|   methods: { | ||||
|     handleDayClick(e) { | ||||
| }); | ||||
|  | ||||
| function handleDayClick(e: Event & {date: Date}) { | ||||
|   // Reset filter if same date is clicked | ||||
|   const params = new URLSearchParams(document.location.search); | ||||
|   const queryDate = params.get('date'); | ||||
|   // Timezone has to be stripped because toISOString() converts to UTC | ||||
|       const clickedDate = new Date(e.date - (e.date.getTimezoneOffset() * 60000)).toISOString().substring(0, 10); | ||||
|   const clickedDate = new Date(e.date.getTime() - (e.date.getTimezoneOffset() * 60000)).toISOString().substring(0, 10); | ||||
|  | ||||
|   if (queryDate && queryDate === clickedDate) { | ||||
|     params.delete('date'); | ||||
| @@ -50,9 +50,7 @@ export default { | ||||
|  | ||||
|   const newSearch = params.toString(); | ||||
|   window.location.search = newSearch.length ? `?${newSearch}` : ''; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| } | ||||
| </script> | ||||
| <template> | ||||
|   <div class="total-contributions"> | ||||
|   | ||||
| @@ -1,100 +1,96 @@ | ||||
| <script lang="ts"> | ||||
| <script lang="ts" setup> | ||||
| import {SvgIcon} from '../svg.ts'; | ||||
| import {GET} from '../modules/fetch.ts'; | ||||
| import {computed, onMounted, ref} from 'vue'; | ||||
| import type {Issue} from '../types'; | ||||
|  | ||||
| const {appSubUrl, i18n} = window.config; | ||||
|  | ||||
| export default { | ||||
|   components: {SvgIcon}, | ||||
|   data: () => ({ | ||||
|     loading: false, | ||||
|     issue: null, | ||||
|     renderedLabels: '', | ||||
|     i18nErrorOccurred: i18n.error_occurred, | ||||
|     i18nErrorMessage: null, | ||||
|   }), | ||||
|   computed: { | ||||
|     createdAt() { | ||||
|       return new Date(this.issue.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'}); | ||||
|     }, | ||||
| const loading = ref(false); | ||||
| const issue = ref(null); | ||||
| const renderedLabels = ref(''); | ||||
| const i18nErrorOccurred = i18n.error_occurred; | ||||
| const i18nErrorMessage = ref(null); | ||||
|  | ||||
|     body() { | ||||
|       const body = this.issue.body.replace(/\n+/g, ' '); | ||||
| const createdAt = computed(() => new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'})); | ||||
| const body = computed(() => { | ||||
|   const body = issue.value.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') { | ||||
|           if (this.issue.pull_request.draft === true) { | ||||
| function getIssueIcon(issue: Issue) { | ||||
|   if (issue.pull_request) { | ||||
|     if (issue.state === 'open') { | ||||
|       if (issue.pull_request.draft === true) { | ||||
|         return 'octicon-git-pull-request-draft'; // WIP PR | ||||
|       } | ||||
|       return 'octicon-git-pull-request'; // Open PR | ||||
|         } else if (this.issue.pull_request.merged === true) { | ||||
|     } else if (issue.pull_request.merged === true) { | ||||
|       return 'octicon-git-merge'; // Merged PR | ||||
|     } | ||||
|     return 'octicon-git-pull-request'; // Closed PR | ||||
|       } else if (this.issue.state === 'open') { | ||||
|   } else if (issue.state === 'open') { | ||||
|     return 'octicon-issue-opened'; // Open Issue | ||||
|   } | ||||
|   return 'octicon-issue-closed'; // Closed Issue | ||||
|     }, | ||||
| } | ||||
|  | ||||
|     color() { | ||||
|       if (this.issue.pull_request !== null) { | ||||
|         if (this.issue.pull_request.draft === true) { | ||||
| function getIssueColor(issue: Issue) { | ||||
|   if (issue.pull_request) { | ||||
|     if (issue.pull_request.draft === true) { | ||||
|       return 'grey'; // WIP PR | ||||
|         } else if (this.issue.pull_request.merged === true) { | ||||
|     } else if (issue.pull_request.merged === true) { | ||||
|       return 'purple'; // Merged PR | ||||
|     } | ||||
|   } | ||||
|       if (this.issue.state === 'open') { | ||||
|   if (issue.state === 'open') { | ||||
|     return 'green'; // Open Issue | ||||
|   } | ||||
|   return 'red'; // Closed Issue | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.$refs.root.addEventListener('ce-load-context-popup', (e) => { | ||||
| } | ||||
|  | ||||
| const root = ref<HTMLElement | null>(null); | ||||
|  | ||||
| onMounted(() => { | ||||
|   root.value.addEventListener('ce-load-context-popup', (e: CustomEvent) => { | ||||
|     const data = e.detail; | ||||
|       if (!this.loading && this.issue === null) { | ||||
|         this.load(data); | ||||
|     if (!loading.value && issue.value === null) { | ||||
|       load(data); | ||||
|     } | ||||
|   }); | ||||
|   }, | ||||
|   methods: { | ||||
|     async load(data) { | ||||
|       this.loading = true; | ||||
|       this.i18nErrorMessage = null; | ||||
| }); | ||||
|  | ||||
| async function load(data) { | ||||
|   loading.value = true; | ||||
|   i18nErrorMessage.value = null; | ||||
|  | ||||
|   try { | ||||
|     const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`); // backend: GetIssueInfo | ||||
|     const respJson = await response.json(); | ||||
|     if (!response.ok) { | ||||
|           this.i18nErrorMessage = respJson.message ?? i18n.network_error; | ||||
|       i18nErrorMessage.value = respJson.message ?? i18n.network_error; | ||||
|       return; | ||||
|     } | ||||
|         this.issue = respJson.convertedIssue; | ||||
|         this.renderedLabels = respJson.renderedLabels; | ||||
|     issue.value = respJson.convertedIssue; | ||||
|     renderedLabels.value = respJson.renderedLabels; | ||||
|   } catch { | ||||
|         this.i18nErrorMessage = i18n.network_error; | ||||
|     i18nErrorMessage.value = i18n.network_error; | ||||
|   } finally { | ||||
|         this.loading = false; | ||||
|     loading.value = false; | ||||
|   } | ||||
| } | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div ref="root"> | ||||
|     <div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/> | ||||
|     <div v-if="!loading && issue !== null" class="tw-flex tw-flex-col tw-gap-2"> | ||||
|       <div class="tw-text-12">{{ issue.repository.full_name }} on {{ createdAt }}</div> | ||||
|       <div class="flex-text-block"> | ||||
|         <svg-icon :name="icon" :class="['text', color]"/> | ||||
|         <svg-icon :name="getIssueIcon(issue)" :class="['text', getIssueColor(issue)]"/> | ||||
|         <span class="issue-title tw-font-semibold tw-break-anywhere"> | ||||
|           {{ issue.title }} | ||||
|           <span class="index">#{{ issue.number }}</span> | ||||
|   | ||||
| @@ -1,22 +1,23 @@ | ||||
| <script lang="ts"> | ||||
| <script lang="ts" setup> | ||||
| import {onMounted, onUnmounted} from 'vue'; | ||||
| import {loadMoreFiles} from '../features/repo-diff.ts'; | ||||
| import {diffTreeStore} from '../modules/stores.ts'; | ||||
|  | ||||
| export default { | ||||
|   data: () => { | ||||
|     return {store: diffTreeStore()}; | ||||
|   }, | ||||
|   mounted() { | ||||
|     document.querySelector('#show-file-list-btn').addEventListener('click', this.toggleFileList); | ||||
|   }, | ||||
|   unmounted() { | ||||
|     document.querySelector('#show-file-list-btn').removeEventListener('click', this.toggleFileList); | ||||
|   }, | ||||
|   methods: { | ||||
|     toggleFileList() { | ||||
|       this.store.fileListIsVisible = !this.store.fileListIsVisible; | ||||
|     }, | ||||
|     diffTypeToString(pType) { | ||||
| const store = diffTreeStore(); | ||||
|  | ||||
| onMounted(() => { | ||||
|   document.querySelector('#show-file-list-btn').addEventListener('click', toggleFileList); | ||||
| }); | ||||
|  | ||||
| onUnmounted(() => { | ||||
|   document.querySelector('#show-file-list-btn').removeEventListener('click', toggleFileList); | ||||
| }); | ||||
|  | ||||
| function toggleFileList() { | ||||
|   store.fileListIsVisible = !store.fileListIsVisible; | ||||
| } | ||||
|  | ||||
| function diffTypeToString(pType) { | ||||
|   const diffTypes = { | ||||
|     1: 'add', | ||||
|     2: 'modify', | ||||
| @@ -25,16 +26,17 @@ export default { | ||||
|     5: 'copy', | ||||
|   }; | ||||
|   return diffTypes[pType]; | ||||
|     }, | ||||
|     diffStatsWidth(adds, dels) { | ||||
| } | ||||
|  | ||||
| function diffStatsWidth(adds, dels) { | ||||
|   return `${adds / (adds + dels) * 100}%`; | ||||
|     }, | ||||
|     loadMoreData() { | ||||
|       loadMoreFiles(this.store.linkLoadMore); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| } | ||||
|  | ||||
| function loadMoreData() { | ||||
|   loadMoreFiles(store.linkLoadMore); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <ol class="diff-stats tw-m-0" ref="root" v-if="store.fileListIsVisible"> | ||||
|     <li v-for="file in store.files" :key="file.NameHash"> | ||||
|   | ||||
| @@ -1,21 +1,18 @@ | ||||
| <script lang="ts"> | ||||
| <script lang="ts" setup> | ||||
| import DiffFileTreeItem from './DiffFileTreeItem.vue'; | ||||
| import {loadMoreFiles} from '../features/repo-diff.ts'; | ||||
| import {toggleElem} from '../utils/dom.ts'; | ||||
| import {diffTreeStore} from '../modules/stores.ts'; | ||||
| import {setFileFolding} from '../features/file-fold.ts'; | ||||
| import {computed, onMounted, onUnmounted} from 'vue'; | ||||
|  | ||||
| const LOCAL_STORAGE_KEY = 'diff_file_tree_visible'; | ||||
|  | ||||
| export default { | ||||
|   components: {DiffFileTreeItem}, | ||||
|   data: () => { | ||||
|     return {store: diffTreeStore()}; | ||||
|   }, | ||||
|   computed: { | ||||
|     fileTree() { | ||||
| const store = diffTreeStore(); | ||||
|  | ||||
| const fileTree = computed(() => { | ||||
|   const result = []; | ||||
|       for (const file of this.store.files) { | ||||
|   for (const file of store.files) { | ||||
|     // Split file into directories | ||||
|     const splits = file.Name.split('/'); | ||||
|     let index = 0; | ||||
| @@ -31,6 +28,11 @@ export default { | ||||
|         name: split, | ||||
|         children: [], | ||||
|         isFile, | ||||
|       } as { | ||||
|         name: string, | ||||
|         children: any[], | ||||
|         isFile: boolean, | ||||
|         file?: any, | ||||
|       }; | ||||
|  | ||||
|       if (isFile === true) { | ||||
| @@ -74,42 +76,47 @@ export default { | ||||
|   // reduce the depth of our tree. | ||||
|   mergeChildIfOnlyOneDir(result); | ||||
|   return result; | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     // Default to true if unset | ||||
|     this.store.fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) !== 'false'; | ||||
|     document.querySelector('.diff-toggle-file-tree-button').addEventListener('click', this.toggleVisibility); | ||||
| }); | ||||
|  | ||||
|     this.hashChangeListener = () => { | ||||
|       this.store.selectedItem = window.location.hash; | ||||
|       this.expandSelectedFile(); | ||||
|     }; | ||||
|     this.hashChangeListener(); | ||||
|     window.addEventListener('hashchange', this.hashChangeListener); | ||||
|   }, | ||||
|   unmounted() { | ||||
|     document.querySelector('.diff-toggle-file-tree-button').removeEventListener('click', this.toggleVisibility); | ||||
|     window.removeEventListener('hashchange', this.hashChangeListener); | ||||
|   }, | ||||
|   methods: { | ||||
|     expandSelectedFile() { | ||||
| onMounted(() => { | ||||
|   // Default to true if unset | ||||
|   store.fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) !== 'false'; | ||||
|   document.querySelector('.diff-toggle-file-tree-button').addEventListener('click', toggleVisibility); | ||||
|  | ||||
|   hashChangeListener(); | ||||
|   window.addEventListener('hashchange', hashChangeListener); | ||||
| }); | ||||
|  | ||||
| onUnmounted(() => { | ||||
|   document.querySelector('.diff-toggle-file-tree-button').removeEventListener('click', toggleVisibility); | ||||
|   window.removeEventListener('hashchange', hashChangeListener); | ||||
| }); | ||||
|  | ||||
| function hashChangeListener() { | ||||
|   store.selectedItem = window.location.hash; | ||||
|   expandSelectedFile(); | ||||
| } | ||||
|  | ||||
| function expandSelectedFile() { | ||||
|   // expand file if the selected file is folded | ||||
|       if (this.store.selectedItem) { | ||||
|         const box = document.querySelector(this.store.selectedItem); | ||||
|   if (store.selectedItem) { | ||||
|     const box = document.querySelector(store.selectedItem); | ||||
|     const folded = box?.getAttribute('data-folded') === 'true'; | ||||
|     if (folded) setFileFolding(box, box.querySelector('.fold-file'), false); | ||||
|   } | ||||
|     }, | ||||
|     toggleVisibility() { | ||||
|       this.updateVisibility(!this.store.fileTreeIsVisible); | ||||
|     }, | ||||
|     updateVisibility(visible) { | ||||
|       this.store.fileTreeIsVisible = visible; | ||||
|       localStorage.setItem(LOCAL_STORAGE_KEY, this.store.fileTreeIsVisible); | ||||
|       this.updateState(this.store.fileTreeIsVisible); | ||||
|     }, | ||||
|     updateState(visible) { | ||||
| } | ||||
|  | ||||
| function toggleVisibility() { | ||||
|   updateVisibility(!store.fileTreeIsVisible); | ||||
| } | ||||
|  | ||||
| function updateVisibility(visible) { | ||||
|   store.fileTreeIsVisible = visible; | ||||
|   localStorage.setItem(LOCAL_STORAGE_KEY, store.fileTreeIsVisible); | ||||
|   updateState(store.fileTreeIsVisible); | ||||
| } | ||||
|  | ||||
| function updateState(visible) { | ||||
|   const btn = document.querySelector('.diff-toggle-file-tree-button'); | ||||
|   const [toShow, toHide] = btn.querySelectorAll('.icon'); | ||||
|   const tree = document.querySelector('#diff-file-tree'); | ||||
| @@ -118,13 +125,13 @@ export default { | ||||
|   toggleElem(tree, visible); | ||||
|   toggleElem(toShow, !visible); | ||||
|   toggleElem(toHide, visible); | ||||
|     }, | ||||
|     loadMoreData() { | ||||
|       loadMoreFiles(this.store.linkLoadMore); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| } | ||||
|  | ||||
| function loadMoreData() { | ||||
|   loadMoreFiles(store.linkLoadMore); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div v-if="store.fileTreeIsVisible" class="diff-file-tree-items"> | ||||
|     <!-- only render the tree if we're visible. in many cases this is something that doesn't change very often --> | ||||
| @@ -134,6 +141,7 @@ export default { | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style scoped> | ||||
| .diff-file-tree-items { | ||||
|   display: flex; | ||||
|   | ||||
| @@ -1,21 +1,30 @@ | ||||
| <script lang="ts"> | ||||
| <script lang="ts" setup> | ||||
| import {SvgIcon} from '../svg.ts'; | ||||
| import {diffTreeStore} from '../modules/stores.ts'; | ||||
| import {ref} from 'vue'; | ||||
|  | ||||
| export default { | ||||
|   components: {SvgIcon}, | ||||
|   props: { | ||||
|     item: { | ||||
|       type: Object, | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   data: () => ({ | ||||
|     store: diffTreeStore(), | ||||
|     collapsed: false, | ||||
|   }), | ||||
|   methods: { | ||||
|     getIconForDiffType(pType) { | ||||
| type File = { | ||||
|   Name: string; | ||||
|   NameHash: string; | ||||
|   Type: number; | ||||
|   IsViewed: boolean; | ||||
| } | ||||
|  | ||||
| type Item = { | ||||
|   name: string; | ||||
|   isFile: boolean; | ||||
|   file?: File; | ||||
|   children?: Item[]; | ||||
| }; | ||||
|  | ||||
| defineProps<{ | ||||
|   item: Item, | ||||
| }>(); | ||||
|  | ||||
| const store = diffTreeStore(); | ||||
| const collapsed = ref(false); | ||||
|  | ||||
| function getIconForDiffType(pType) { | ||||
|   const diffTypes = { | ||||
|     1: {name: 'octicon-diff-added', classes: ['text', 'green']}, | ||||
|     2: {name: 'octicon-diff-modified', classes: ['text', 'yellow']}, | ||||
| @@ -24,10 +33,9 @@ export default { | ||||
|     5: {name: 'octicon-diff-renamed', classes: ['text', 'green']}, // there is no octicon for copied, so renamed should be ok | ||||
|   }; | ||||
|   return diffTypes[pType]; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <!--title instead of tooltip above as the tooltip needs too much work with the current methods, i.e. not being loaded or staying open for "too long"--> | ||||
|   <a | ||||
|   | ||||
| @@ -1,84 +1,83 @@ | ||||
| <script lang="ts"> | ||||
| <script lang="ts" setup> | ||||
| import {computed, onMounted, onUnmounted, ref, watch} from 'vue'; | ||||
| import {SvgIcon} from '../svg.ts'; | ||||
| import {toggleElem} from '../utils/dom.ts'; | ||||
|  | ||||
| const {csrfToken, pageData} = window.config; | ||||
|  | ||||
| export default { | ||||
|   components: {SvgIcon}, | ||||
|   data: () => ({ | ||||
|     csrfToken, | ||||
|     mergeForm: pageData.pullRequestMergeForm, | ||||
| const mergeForm = ref(pageData.pullRequestMergeForm); | ||||
|  | ||||
|     mergeTitleFieldValue: '', | ||||
|     mergeMessageFieldValue: '', | ||||
|     deleteBranchAfterMerge: false, | ||||
|     autoMergeWhenSucceed: false, | ||||
| const mergeTitleFieldValue = ref(''); | ||||
| const mergeMessageFieldValue = ref(''); | ||||
| const deleteBranchAfterMerge = ref(false); | ||||
| const autoMergeWhenSucceed = ref(false); | ||||
|  | ||||
|     mergeStyle: '', | ||||
|     mergeStyleDetail: { // dummy only, these values will come from one of the mergeForm.mergeStyles | ||||
| const mergeStyle = ref(''); | ||||
| const mergeStyleDetail = ref({ | ||||
|   hideMergeMessageTexts: false, | ||||
|   textDoMerge: '', | ||||
|   mergeTitleFieldText: '', | ||||
|   mergeMessageFieldText: '', | ||||
|   hideAutoMerge: false, | ||||
|     }, | ||||
|     mergeStyleAllowedCount: 0, | ||||
| }); | ||||
|  | ||||
|     showMergeStyleMenu: false, | ||||
|     showActionForm: false, | ||||
|   }), | ||||
|   computed: { | ||||
|     mergeButtonStyleClass() { | ||||
|       if (this.mergeForm.allOverridableChecksOk) return 'primary'; | ||||
|       return this.autoMergeWhenSucceed ? 'primary' : 'red'; | ||||
|     }, | ||||
|     forceMerge() { | ||||
|       return this.mergeForm.canMergeNow && !this.mergeForm.allOverridableChecksOk; | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|     mergeStyle(val) { | ||||
|       this.mergeStyleDetail = this.mergeForm.mergeStyles.find((e) => e.name === val); | ||||
| const mergeStyleAllowedCount = ref(0); | ||||
|  | ||||
| const showMergeStyleMenu = ref(false); | ||||
| const showActionForm = ref(false); | ||||
|  | ||||
| const mergeButtonStyleClass = computed(() => { | ||||
|   if (mergeForm.value.allOverridableChecksOk) return 'primary'; | ||||
|   return autoMergeWhenSucceed.value ? 'primary' : 'red'; | ||||
| }); | ||||
|  | ||||
| const forceMerge = computed(() => { | ||||
|   return mergeForm.value.canMergeNow && !mergeForm.value.allOverridableChecksOk; | ||||
| }); | ||||
|  | ||||
| watch(mergeStyle, (val) => { | ||||
|   mergeStyleDetail.value = mergeForm.value.mergeStyles.find((e) => e.name === val); | ||||
|   for (const elem of document.querySelectorAll('[data-pull-merge-style]')) { | ||||
|     toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val); | ||||
|   } | ||||
|     }, | ||||
|   }, | ||||
|   created() { | ||||
|     this.mergeStyleAllowedCount = this.mergeForm.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0); | ||||
| }); | ||||
|  | ||||
|     let mergeStyle = this.mergeForm.mergeStyles.find((e) => e.allowed && e.name === this.mergeForm.defaultMergeStyle)?.name; | ||||
|     if (!mergeStyle) mergeStyle = this.mergeForm.mergeStyles.find((e) => e.allowed)?.name; | ||||
|     this.switchMergeStyle(mergeStyle, !this.mergeForm.canMergeNow); | ||||
|   }, | ||||
|   mounted() { | ||||
|     document.addEventListener('mouseup', this.hideMergeStyleMenu); | ||||
|   }, | ||||
|   unmounted() { | ||||
|     document.removeEventListener('mouseup', this.hideMergeStyleMenu); | ||||
|   }, | ||||
|   methods: { | ||||
|     hideMergeStyleMenu() { | ||||
|       this.showMergeStyleMenu = false; | ||||
|     }, | ||||
|     toggleActionForm(show) { | ||||
|       this.showActionForm = show; | ||||
| onMounted(() => { | ||||
|   mergeStyleAllowedCount.value = mergeForm.value.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0); | ||||
|  | ||||
|   let mergeStyle = mergeForm.value.mergeStyles.find((e) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name; | ||||
|   if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e) => e.allowed)?.name; | ||||
|   switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow); | ||||
|  | ||||
|   document.addEventListener('mouseup', hideMergeStyleMenu); | ||||
| }); | ||||
|  | ||||
| onUnmounted(() => { | ||||
|   document.removeEventListener('mouseup', hideMergeStyleMenu); | ||||
| }); | ||||
|  | ||||
| function hideMergeStyleMenu() { | ||||
|   showMergeStyleMenu.value = false; | ||||
| } | ||||
|  | ||||
| function toggleActionForm(show: boolean) { | ||||
|   showActionForm.value = show; | ||||
|   if (!show) return; | ||||
|       this.deleteBranchAfterMerge = this.mergeForm.defaultDeleteBranchAfterMerge; | ||||
|       this.mergeTitleFieldValue = this.mergeStyleDetail.mergeTitleFieldText; | ||||
|       this.mergeMessageFieldValue = this.mergeStyleDetail.mergeMessageFieldText; | ||||
|     }, | ||||
|     switchMergeStyle(name, autoMerge = false) { | ||||
|       this.mergeStyle = name; | ||||
|       this.autoMergeWhenSucceed = autoMerge; | ||||
|     }, | ||||
|     clearMergeMessage() { | ||||
|       this.mergeMessageFieldValue = this.mergeForm.defaultMergeMessage; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|   deleteBranchAfterMerge.value = mergeForm.value.defaultDeleteBranchAfterMerge; | ||||
|   mergeTitleFieldValue.value = mergeStyleDetail.value.mergeTitleFieldText; | ||||
|   mergeMessageFieldValue.value = mergeStyleDetail.value.mergeMessageFieldText; | ||||
| } | ||||
|  | ||||
| function switchMergeStyle(name, autoMerge = false) { | ||||
|   mergeStyle.value = name; | ||||
|   autoMergeWhenSucceed.value = autoMerge; | ||||
| } | ||||
|  | ||||
| function clearMergeMessage() { | ||||
|   mergeMessageFieldValue.value = mergeForm.value.defaultMergeMessage; | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <!-- | ||||
|   if this component is shown, either the user is an admin (can do a merge without checks), or they are a writer who has the permission to do a merge | ||||
| @@ -186,6 +185,7 @@ export default { | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style scoped> | ||||
| /* to keep UI the same, at the moment we are still using some Fomantic UI styles, but we do not use their scripts, so we need to fine tune some styles */ | ||||
| .ui.dropdown .menu.show { | ||||
|   | ||||
| @@ -1,15 +1,12 @@ | ||||
| <script lang="ts"> | ||||
| <script lang="ts" setup> | ||||
| import {VueBarGraph} from 'vue-bar-graph'; | ||||
| import {createApp} from 'vue'; | ||||
| import {computed, onMounted, ref} from 'vue'; | ||||
|  | ||||
| const sfc = { | ||||
|   components: {VueBarGraph}, | ||||
|   data: () => ({ | ||||
|     colors: { | ||||
| const colors = ref({ | ||||
|   barColor: 'green', | ||||
|   textColor: 'black', | ||||
|   textAltColor: 'white', | ||||
|     }, | ||||
| }); | ||||
|  | ||||
| // possible keys: | ||||
| // * avatar_link: (...) | ||||
| @@ -17,52 +14,49 @@ const sfc = { | ||||
| // * home_link: (...) | ||||
| // * login: (...) | ||||
| // * name: (...) | ||||
|     activityTopAuthors: window.config.pageData.repoActivityTopAuthors || [], | ||||
|   }), | ||||
|   computed: { | ||||
|     graphPoints() { | ||||
|       return this.activityTopAuthors.map((item) => { | ||||
| const activityTopAuthors = window.config.pageData.repoActivityTopAuthors || []; | ||||
|  | ||||
| const graphPoints = computed(() => { | ||||
|   return activityTopAuthors.value.map((item) => { | ||||
|     return { | ||||
|       value: item.commits, | ||||
|       label: item.name, | ||||
|     }; | ||||
|   }); | ||||
|     }, | ||||
|     graphAuthors() { | ||||
|       return this.activityTopAuthors.map((item, idx) => { | ||||
| }); | ||||
|  | ||||
| const graphAuthors = computed(() => { | ||||
|   return activityTopAuthors.value.map((item, idx) => { | ||||
|     return { | ||||
|       position: idx + 1, | ||||
|       ...item, | ||||
|     }; | ||||
|   }); | ||||
|     }, | ||||
|     graphWidth() { | ||||
|       return this.activityTopAuthors.length * 40; | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     const refStyle = window.getComputedStyle(this.$refs.style); | ||||
|     const refAltStyle = window.getComputedStyle(this.$refs.altStyle); | ||||
| }); | ||||
|  | ||||
|     this.colors.barColor = refStyle.backgroundColor; | ||||
|     this.colors.textColor = refStyle.color; | ||||
|     this.colors.textAltColor = refAltStyle.color; | ||||
|   }, | ||||
| const graphWidth = computed(() => { | ||||
|   return activityTopAuthors.value.length * 40; | ||||
| }); | ||||
|  | ||||
| const styleElement = ref<HTMLElement | null>(null); | ||||
| const altStyleElement = ref<HTMLElement | null>(null); | ||||
|  | ||||
| onMounted(() => { | ||||
|   const refStyle = window.getComputedStyle(styleElement.value); | ||||
|   const refAltStyle = window.getComputedStyle(altStyleElement.value); | ||||
|  | ||||
|   colors.value = { | ||||
|     barColor: refStyle.backgroundColor, | ||||
|     textColor: refStyle.color, | ||||
|     textAltColor: refAltStyle.color, | ||||
|   }; | ||||
|  | ||||
| export function initRepoActivityTopAuthorsChart() { | ||||
|   const el = document.querySelector('#repo-activity-top-authors-chart'); | ||||
|   if (el) { | ||||
|     createApp(sfc).mount(el); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default sfc; // activate the IDE's Vue plugin | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <div class="activity-bar-graph" ref="style" style="width: 0; height: 0;"/> | ||||
|     <div class="activity-bar-graph-alt" ref="altStyle" style="width: 0; height: 0;"/> | ||||
|     <div class="activity-bar-graph" ref="styleElement" style="width: 0; height: 0;"/> | ||||
|     <div class="activity-bar-graph-alt" ref="altStyleElement" style="width: 0; height: 0;"/> | ||||
|     <vue-bar-graph | ||||
|       :points="graphPoints" | ||||
|       :show-x-axis="true" | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <script lang="ts"> | ||||
| <script lang="ts" setup> | ||||
| import {SvgIcon} from '../svg.ts'; | ||||
| import { | ||||
|   Chart, | ||||
| @@ -15,10 +15,12 @@ import { | ||||
|   startDaysBetween, | ||||
|   firstStartDateAfterDate, | ||||
|   fillEmptyStartDaysWithZeroes, | ||||
|   type DayData, | ||||
| } from '../utils/time.ts'; | ||||
| import {chartJsColors} from '../utils/color.ts'; | ||||
| import {sleep} from '../utils.ts'; | ||||
| import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; | ||||
| import {onMounted, ref} from 'vue'; | ||||
|  | ||||
| const {pageData} = window.config; | ||||
|  | ||||
| @@ -34,53 +36,52 @@ Chart.register( | ||||
|   Filler, | ||||
| ); | ||||
|  | ||||
| export default { | ||||
|   components: {ChartLine, SvgIcon}, | ||||
|   props: { | ||||
| defineProps<{ | ||||
|   locale: { | ||||
|       type: Object, | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   data: () => ({ | ||||
|     isLoading: false, | ||||
|     errorText: '', | ||||
|     repoLink: pageData.repoLink || [], | ||||
|     data: [], | ||||
|   }), | ||||
|   mounted() { | ||||
|     this.fetchGraphData(); | ||||
|   }, | ||||
|   methods: { | ||||
|     async fetchGraphData() { | ||||
|       this.isLoading = true; | ||||
|     loadingTitle: string; | ||||
|     loadingTitleFailed: string; | ||||
|     loadingInfo: string; | ||||
|   }; | ||||
| }>(); | ||||
|  | ||||
| const isLoading = ref(false); | ||||
| const errorText = ref(''); | ||||
| const repoLink = ref(pageData.repoLink || []); | ||||
| const data = ref<DayData[]>([]); | ||||
|  | ||||
| onMounted(() => { | ||||
|   fetchGraphData(); | ||||
| }); | ||||
|  | ||||
| async function fetchGraphData() { | ||||
|   isLoading.value = true; | ||||
|   try { | ||||
|         let response; | ||||
|     let response: Response; | ||||
|     do { | ||||
|           response = await GET(`${this.repoLink}/activity/code-frequency/data`); | ||||
|       response = await GET(`${repoLink.value}/activity/code-frequency/data`); | ||||
|       if (response.status === 202) { | ||||
|         await sleep(1000); // wait for 1 second before retrying | ||||
|       } | ||||
|     } while (response.status === 202); | ||||
|     if (response.ok) { | ||||
|           this.data = await response.json(); | ||||
|           const weekValues = Object.values(this.data); | ||||
|       data.value = await response.json(); | ||||
|       const weekValues = Object.values(data.value); | ||||
|       const start = weekValues[0].week; | ||||
|       const end = firstStartDateAfterDate(new Date()); | ||||
|       const startDays = startDaysBetween(start, end); | ||||
|           this.data = fillEmptyStartDaysWithZeroes(startDays, this.data); | ||||
|           this.errorText = ''; | ||||
|       data.value = fillEmptyStartDaysWithZeroes(startDays, data.value); | ||||
|       errorText.value = ''; | ||||
|     } else { | ||||
|           this.errorText = response.statusText; | ||||
|       errorText.value = response.statusText; | ||||
|     } | ||||
|   } catch (err) { | ||||
|         this.errorText = err.message; | ||||
|     errorText.value = err.message; | ||||
|   } finally { | ||||
|         this.isLoading = false; | ||||
|     isLoading.value = false; | ||||
|   } | ||||
| } | ||||
|     }, | ||||
|  | ||||
|     toGraphData(data) { | ||||
| function toGraphData(data) { | ||||
|   return { | ||||
|     datasets: [ | ||||
|       { | ||||
| @@ -105,10 +106,9 @@ export default { | ||||
|       }, | ||||
|     ], | ||||
|   }; | ||||
|     }, | ||||
| } | ||||
|  | ||||
|     getOptions() { | ||||
|       return { | ||||
| const options = { | ||||
|   responsive: true, | ||||
|   maintainAspectRatio: false, | ||||
|   animation: true, | ||||
| @@ -138,10 +138,8 @@ export default { | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <div class="ui header tw-flex tw-items-center tw-justify-between"> | ||||
| @@ -160,11 +158,12 @@ export default { | ||||
|       </div> | ||||
|       <ChartLine | ||||
|         v-memo="data" v-if="data.length !== 0" | ||||
|         :data="toGraphData(data)" :options="getOptions()" | ||||
|         :data="toGraphData(data)" :options="options" | ||||
|       /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style scoped> | ||||
| .main-graph { | ||||
|   height: 440px; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <script lang="ts"> | ||||
| <script lang="ts" setup> | ||||
| import {SvgIcon} from '../svg.ts'; | ||||
| import { | ||||
|   Chart, | ||||
| @@ -6,6 +6,7 @@ import { | ||||
|   BarElement, | ||||
|   LinearScale, | ||||
|   TimeScale, | ||||
|   type ChartOptions, | ||||
| } from 'chart.js'; | ||||
| import {GET} from '../modules/fetch.ts'; | ||||
| import {Bar} from 'vue-chartjs'; | ||||
| @@ -13,10 +14,12 @@ import { | ||||
|   startDaysBetween, | ||||
|   firstStartDateAfterDate, | ||||
|   fillEmptyStartDaysWithZeroes, | ||||
|   type DayData, | ||||
| } from '../utils/time.ts'; | ||||
| import {chartJsColors} from '../utils/color.ts'; | ||||
| import {sleep} from '../utils.ts'; | ||||
| import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; | ||||
| import {onMounted, ref} from 'vue'; | ||||
|  | ||||
| const {pageData} = window.config; | ||||
|  | ||||
| @@ -30,30 +33,29 @@ Chart.register( | ||||
|   Tooltip, | ||||
| ); | ||||
|  | ||||
| export default { | ||||
|   components: {Bar, SvgIcon}, | ||||
|   props: { | ||||
| defineProps<{ | ||||
|   locale: { | ||||
|       type: Object, | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   data: () => ({ | ||||
|     isLoading: false, | ||||
|     errorText: '', | ||||
|     repoLink: pageData.repoLink || [], | ||||
|     data: [], | ||||
|   }), | ||||
|   mounted() { | ||||
|     this.fetchGraphData(); | ||||
|   }, | ||||
|   methods: { | ||||
|     async fetchGraphData() { | ||||
|       this.isLoading = true; | ||||
|     loadingTitle: string; | ||||
|     loadingTitleFailed: string; | ||||
|     loadingInfo: string; | ||||
|   }; | ||||
| }>(); | ||||
|  | ||||
| const isLoading = ref(false); | ||||
| const errorText = ref(''); | ||||
| const repoLink = ref(pageData.repoLink || []); | ||||
| const data = ref<DayData[]>([]); | ||||
|  | ||||
| onMounted(() => { | ||||
|   fetchGraphData(); | ||||
| }); | ||||
|  | ||||
| async function fetchGraphData() { | ||||
|   isLoading.value = true; | ||||
|   try { | ||||
|         let response; | ||||
|     let response: Response; | ||||
|     do { | ||||
|           response = await GET(`${this.repoLink}/activity/recent-commits/data`); | ||||
|       response = await GET(`${repoLink.value}/activity/recent-commits/data`); | ||||
|       if (response.status === 202) { | ||||
|         await sleep(1000); // wait for 1 second before retrying | ||||
|       } | ||||
| @@ -63,19 +65,19 @@ export default { | ||||
|       const start = Object.values(data)[0].week; | ||||
|       const end = firstStartDateAfterDate(new Date()); | ||||
|       const startDays = startDaysBetween(start, end); | ||||
|           this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52); | ||||
|           this.errorText = ''; | ||||
|       data.value = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52); | ||||
|       errorText.value = ''; | ||||
|     } else { | ||||
|           this.errorText = response.statusText; | ||||
|       errorText.value = response.statusText; | ||||
|     } | ||||
|   } catch (err) { | ||||
|         this.errorText = err.message; | ||||
|     errorText.value = err.message; | ||||
|   } finally { | ||||
|         this.isLoading = false; | ||||
|     isLoading.value = false; | ||||
|   } | ||||
| } | ||||
|     }, | ||||
|  | ||||
|     toGraphData(data) { | ||||
| function toGraphData(data) { | ||||
|   return { | ||||
|     datasets: [ | ||||
|       { | ||||
| @@ -87,10 +89,9 @@ export default { | ||||
|       }, | ||||
|     ], | ||||
|   }; | ||||
|     }, | ||||
| } | ||||
|  | ||||
|     getOptions() { | ||||
|       return { | ||||
| const options = { | ||||
|   responsive: true, | ||||
|   maintainAspectRatio: false, | ||||
|   animation: true, | ||||
| @@ -114,11 +115,9 @@ export default { | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|       }; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| } satisfies ChartOptions; | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <div class="ui header tw-flex tw-items-center tw-justify-between"> | ||||
| @@ -137,7 +136,7 @@ export default { | ||||
|       </div> | ||||
|       <Bar | ||||
|         v-memo="data" v-if="data.length !== 0" | ||||
|         :data="toGraphData(data)" :options="getOptions()" | ||||
|         :data="toGraphData(data)" :options="options" | ||||
|       /> | ||||
|     </div> | ||||
|   </div> | ||||
|   | ||||
| @@ -1,32 +1,19 @@ | ||||
| <script lang="ts"> | ||||
| <script lang="ts" setup> | ||||
| import {computed, onMounted, onUnmounted} from 'vue'; | ||||
| import {hideElem, showElem} from '../utils/dom.ts'; | ||||
|  | ||||
| const sfc = { | ||||
|   props: { | ||||
|     isAdmin: { | ||||
|       type: Boolean, | ||||
|       required: true, | ||||
|     }, | ||||
|     noAccessLabel: { | ||||
|       type: String, | ||||
|       required: true, | ||||
|     }, | ||||
|     readLabel: { | ||||
|       type: String, | ||||
|       required: true, | ||||
|     }, | ||||
|     writeLabel: { | ||||
|       type: String, | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
| const props = defineProps<{ | ||||
|   isAdmin: boolean; | ||||
|   noAccessLabel: string; | ||||
|   readLabel: string; | ||||
|   writeLabel: string; | ||||
| }>(); | ||||
|  | ||||
|   computed: { | ||||
|     categories() { | ||||
| const categories = computed(() => { | ||||
|   const categories = [ | ||||
|     'activitypub', | ||||
|   ]; | ||||
|       if (this.isAdmin) { | ||||
|   if (props.isAdmin) { | ||||
|     categories.push('admin'); | ||||
|   } | ||||
|   categories.push( | ||||
| @@ -38,41 +25,36 @@ const sfc = { | ||||
|     'repository', | ||||
|     'user'); | ||||
|   return categories; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
|   mounted() { | ||||
|     document.querySelector('#scoped-access-submit').addEventListener('click', this.onClickSubmit); | ||||
|   }, | ||||
| onMounted(() => { | ||||
|   document.querySelector('#scoped-access-submit').addEventListener('click', onClickSubmit); | ||||
| }); | ||||
|  | ||||
|   unmounted() { | ||||
|     document.querySelector('#scoped-access-submit').removeEventListener('click', this.onClickSubmit); | ||||
|   }, | ||||
| onUnmounted(() => { | ||||
|   document.querySelector('#scoped-access-submit').removeEventListener('click', onClickSubmit); | ||||
| }); | ||||
|  | ||||
|   methods: { | ||||
|     onClickSubmit(e) { | ||||
| function onClickSubmit(e) { | ||||
|   e.preventDefault(); | ||||
|  | ||||
|   const warningEl = document.querySelector('#scoped-access-warning'); | ||||
|   // check that at least one scope has been selected | ||||
|       for (const el of document.querySelectorAll('.access-token-select')) { | ||||
|   for (const el of document.querySelectorAll<HTMLInputElement>('.access-token-select')) { | ||||
|     if (el.value) { | ||||
|       // Hide the error if it was visible from previous attempt. | ||||
|       hideElem(warningEl); | ||||
|       // Submit the form. | ||||
|           document.querySelector('#scoped-access-form').submit(); | ||||
|       document.querySelector<HTMLFormElement>('#scoped-access-form').submit(); | ||||
|       // Don't show the warning. | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|   // no scopes selected, show validation error | ||||
|   showElem(warningEl); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export default sfc; | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div v-for="category in categories" :key="category" class="field tw-pl-1 tw-pb-1 access-token-category"> | ||||
|     <label class="category-label" :for="'access-token-scope-' + category"> | ||||
|   | ||||
| @@ -3,6 +3,8 @@ import {hideElem, queryElems, showElem} from '../utils/dom.ts'; | ||||
| import {POST} from '../modules/fetch.ts'; | ||||
| import {showErrorToast} from '../modules/toast.ts'; | ||||
| import {sleep} from '../utils.ts'; | ||||
| import RepoActivityTopAuthors from '../components/RepoActivityTopAuthors.vue'; | ||||
| import {createApp} from 'vue'; | ||||
|  | ||||
| async function onDownloadArchive(e) { | ||||
|   e.preventDefault(); | ||||
| @@ -32,6 +34,13 @@ export function initRepoArchiveLinks() { | ||||
|   queryElems('a.archive-link[href]', (el) => el.addEventListener('click', onDownloadArchive)); | ||||
| } | ||||
|  | ||||
| export function initRepoActivityTopAuthorsChart() { | ||||
|   const el = document.querySelector('#repo-activity-top-authors-chart'); | ||||
|   if (el) { | ||||
|     createApp(RepoActivityTopAuthors).mount(el); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function initRepoCloneLink() { | ||||
|   const $repoCloneSsh = $('#repo-clone-ssh'); | ||||
|   const $repoCloneHttps = $('#repo-clone-https'); | ||||
|   | ||||
| @@ -2,7 +2,6 @@ | ||||
| import './bootstrap.ts'; | ||||
| import './htmx.ts'; | ||||
|  | ||||
| import {initRepoActivityTopAuthorsChart} from './components/RepoActivityTopAuthors.vue'; | ||||
| import {initDashboardRepoList} from './components/DashboardRepoList.vue'; | ||||
|  | ||||
| import {initGlobalCopyToClipboardListener} from './features/clipboard.ts'; | ||||
| @@ -42,7 +41,7 @@ import {initRepoTemplateSearch} from './features/repo-template.ts'; | ||||
| import {initRepoCodeView} from './features/repo-code.ts'; | ||||
| import {initSshKeyFormParser} from './features/sshkey-helper.ts'; | ||||
| import {initUserSettings} from './features/user-settings.ts'; | ||||
| import {initRepoArchiveLinks} from './features/repo-common.ts'; | ||||
| import {initRepoActivityTopAuthorsChart, initRepoArchiveLinks} from './features/repo-common.ts'; | ||||
| import {initRepoMigrationStatusChecker} from './features/repo-migrate.ts'; | ||||
| import { | ||||
|   initRepoSettingGitHook, | ||||
|   | ||||
| @@ -36,3 +36,13 @@ export type IssueData = { | ||||
|   type: string, | ||||
|   index: string, | ||||
| } | ||||
|  | ||||
| export type Issue = { | ||||
|   id: number; | ||||
|   title: string; | ||||
|   state: 'open' | 'closed'; | ||||
|   pull_request?: { | ||||
|     draft: boolean; | ||||
|     merged: boolean; | ||||
|   }; | ||||
| }; | ||||
|   | ||||
| @@ -42,14 +42,14 @@ export function firstStartDateAfterDate(inputDate: Date): number { | ||||
|   return resultDate.valueOf(); | ||||
| } | ||||
|  | ||||
| type DayData = { | ||||
| export type DayData = { | ||||
|   week: number, | ||||
|   additions: number, | ||||
|   deletions: number, | ||||
|   commits: number, | ||||
| } | ||||
|  | ||||
| export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayData): DayData[] { | ||||
| export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayData[]): DayData[] { | ||||
|   const result = {}; | ||||
|  | ||||
|   for (const startDay of startDays) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Anbraten
					Anbraten