diff options
author | Anbraten <6918444+anbraten@users.noreply.github.com> | 2024-10-28 21:15:05 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-10-28 20:15:05 +0000 |
commit | 348d1d0f322ca57c459acd902f54821d687ca804 (patch) | |
tree | 442043faa7de26108abd321a34d646afacf66d0d | |
parent | a920fcfd91b1d77cee8bf1143334cba1582b8c5c (diff) | |
download | gitea-348d1d0f322ca57c459acd902f54821d687ca804.tar.gz gitea-348d1d0f322ca57c459acd902f54821d687ca804.zip |
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
-rw-r--r-- | web_src/js/components/ActionRunStatus.vue | 34 | ||||
-rw-r--r-- | web_src/js/components/ActivityHeatmap.vue | 96 | ||||
-rw-r--r-- | web_src/js/components/ContextPopup.vue | 156 | ||||
-rw-r--r-- | web_src/js/components/DiffFileList.vue | 68 | ||||
-rw-r--r-- | web_src/js/components/DiffFileTree.vue | 238 | ||||
-rw-r--r-- | web_src/js/components/DiffFileTreeItem.vue | 58 | ||||
-rw-r--r-- | web_src/js/components/PullRequestMergeForm.vue | 150 | ||||
-rw-r--r-- | web_src/js/components/RepoActivityTopAuthors.vue | 106 | ||||
-rw-r--r-- | web_src/js/components/RepoCodeFrequency.vue | 205 | ||||
-rw-r--r-- | web_src/js/components/RepoRecentCommits.vue | 169 | ||||
-rw-r--r-- | web_src/js/components/ScopedAccessTokenSelector.vue | 116 | ||||
-rw-r--r-- | web_src/js/features/repo-common.ts | 9 | ||||
-rw-r--r-- | web_src/js/index.ts | 3 | ||||
-rw-r--r-- | web_src/js/types.ts | 10 | ||||
-rw-r--r-- | web_src/js/utils/time.ts | 4 |
15 files changed, 708 insertions, 714 deletions
diff --git a/web_src/js/components/ActionRunStatus.vue b/web_src/js/components/ActionRunStatus.vue index 5181c2c475..558b881dfe 100644 --- a/web_src/js/components/ActionRunStatus.vue +++ b/web_src/js/components/ActionRunStatus.vue @@ -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'"/> diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue index 2d84a718e4..eaa9b0ffb1 100644 --- a/web_src/js/components/ActivityHeatmap.vue +++ b/web_src/js/components/ActivityHeatmap.vue @@ -1,58 +1,56 @@ -<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: () => [], - }, - locale: { - type: Object, - default: () => {}, - }, - }, - data: () => ({ - colorRange: [ - 'var(--color-secondary-alpha-60)', - 'var(--color-secondary-alpha-60)', - 'var(--color-primary-light-4)', - 'var(--color-primary-light-2)', - 'var(--color-primary)', - 'var(--color-primary-dark-2)', - 'var(--color-primary-dark-4)', - ], - endDate: new Date(), - }), - mounted() { - // work around issue with first legend color being rendered twice and legend cut off - const legend = document.querySelector('.vch__external-legend-wrapper'); - legend.setAttribute('viewBox', '12 0 80 10'); - legend.style.marginRight = '-12px'; - }, - methods: { - handleDayClick(e) { - // 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); +defineProps<{ + values?: HeatmapValue[]; + locale: { + textTotalContributions: string; + heatMapLocale: Partial<HeatmapLocale>; + noDataText: string; + tooltipUnit: string; + }; +}>(); - if (queryDate && queryDate === clickedDate) { - params.delete('date'); - } else { - params.set('date', clickedDate); - } +const colorRange = [ + 'var(--color-secondary-alpha-60)', + 'var(--color-secondary-alpha-60)', + 'var(--color-primary-light-4)', + 'var(--color-primary-light-2)', + 'var(--color-primary)', + 'var(--color-primary-dark-2)', + 'var(--color-primary-dark-4)', +]; - params.delete('page'); +const endDate = ref(new Date()); - const newSearch = params.toString(); - window.location.search = newSearch.length ? `?${newSearch}` : ''; - }, - }, -}; +onMounted(() => { + // work around issue with first legend color being rendered twice and legend cut off + const legend = document.querySelector<HTMLElement>('.vch__external-legend-wrapper'); + legend.setAttribute('viewBox', '12 0 80 10'); + legend.style.marginRight = '-12px'; +}); + +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.getTime() - (e.date.getTimezoneOffset() * 60000)).toISOString().substring(0, 10); + + if (queryDate && queryDate === clickedDate) { + params.delete('date'); + } else { + params.set('date', clickedDate); + } + + params.delete('page'); + + const newSearch = params.toString(); + window.location.search = newSearch.length ? `?${newSearch}` : ''; +} </script> <template> <div class="total-contributions"> diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue index 2963412893..9fb03dcb7d 100644 --- a/web_src/js/components/ContextPopup.vue +++ b/web_src/js/components/ContextPopup.vue @@ -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, ' '); - if (body.length > 85) { - return `${body.substring(0, 85)}…`; - } - return body; - }, +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) { - return 'octicon-git-pull-request-draft'; // WIP PR - } - 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 +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-issue-closed'; // Closed Issue - }, + return 'octicon-git-pull-request'; // Open PR + } else if (issue.pull_request.merged === true) { + return 'octicon-git-merge'; // Merged PR + } + return 'octicon-git-pull-request'; // Closed PR + } 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) { - return 'grey'; // WIP PR - } else if (this.issue.pull_request.merged === true) { - return 'purple'; // Merged PR - } - } - if (this.issue.state === 'open') { - return 'green'; // Open Issue - } - return 'red'; // Closed Issue - }, - }, - mounted() { - this.$refs.root.addEventListener('ce-load-context-popup', (e) => { - const data = e.detail; - if (!this.loading && this.issue === null) { - this.load(data); - } - }); - }, - methods: { - async load(data) { - this.loading = true; - this.i18nErrorMessage = null; +function getIssueColor(issue: Issue) { + if (issue.pull_request) { + if (issue.pull_request.draft === true) { + return 'grey'; // WIP PR + } else if (issue.pull_request.merged === true) { + return 'purple'; // Merged PR + } + } + if (issue.state === 'open') { + return 'green'; // Open Issue + } + return 'red'; // Closed Issue +} - 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; - return; - } - this.issue = respJson.convertedIssue; - this.renderedLabels = respJson.renderedLabels; - } catch { - this.i18nErrorMessage = i18n.network_error; - } finally { - this.loading = false; - } - }, - }, -}; +const root = ref<HTMLElement | null>(null); + +onMounted(() => { + root.value.addEventListener('ce-load-context-popup', (e: CustomEvent) => { + const data = e.detail; + if (!loading.value && issue.value === null) { + load(data); + } + }); +}); + +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) { + i18nErrorMessage.value = respJson.message ?? i18n.network_error; + return; + } + issue.value = respJson.convertedIssue; + renderedLabels.value = respJson.renderedLabels; + } catch { + i18nErrorMessage.value = i18n.network_error; + } finally { + 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> diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue index 677afd72a3..2888c53d2e 100644 --- a/web_src/js/components/DiffFileList.vue +++ b/web_src/js/components/DiffFileList.vue @@ -1,40 +1,42 @@ -<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 diffTypes = { - 1: 'add', - 2: 'modify', - 3: 'del', - 4: 'rename', - 5: 'copy', - }; - return diffTypes[pType]; - }, - diffStatsWidth(adds, dels) { - return `${adds / (adds + dels) * 100}%`; - }, - loadMoreData() { - loadMoreFiles(this.store.linkLoadMore); - }, - }, -}; +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', + 3: 'del', + 4: 'rename', + 5: 'copy', + }; + return diffTypes[pType]; +} + +function diffStatsWidth(adds, dels) { + return `${adds / (adds + dels) * 100}%`; +} + +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"> diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue index 2262e3e643..9eabc65ae9 100644 --- a/web_src/js/components/DiffFileTree.vue +++ b/web_src/js/components/DiffFileTree.vue @@ -1,130 +1,137 @@ -<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 result = []; - for (const file of this.store.files) { - // Split file into directories - const splits = file.Name.split('/'); - let index = 0; - let parent = null; - let isFile = false; - for (const split of splits) { - index += 1; - // reached the end - if (index === splits.length) { - isFile = true; - } - let newParent = { - name: split, - children: [], - isFile, - }; - - if (isFile === true) { - newParent.file = file; - } - - if (parent) { - // check if the folder already exists - const existingFolder = parent.children.find( - (x) => x.name === split, - ); - if (existingFolder) { - newParent = existingFolder; - } else { - parent.children.push(newParent); - } - } else { - const existingFolder = result.find((x) => x.name === split); - if (existingFolder) { - newParent = existingFolder; - } else { - result.push(newParent); - } - } - parent = newParent; - } +const store = diffTreeStore(); + +const fileTree = computed(() => { + const result = []; + for (const file of store.files) { + // Split file into directories + const splits = file.Name.split('/'); + let index = 0; + let parent = null; + let isFile = false; + for (const split of splits) { + index += 1; + // reached the end + if (index === splits.length) { + isFile = true; } - const mergeChildIfOnlyOneDir = (entries) => { - for (const entry of entries) { - if (entry.children) { - mergeChildIfOnlyOneDir(entry.children); - } - if (entry.children.length === 1 && entry.children[0].isFile === false) { - // Merge it to the parent - entry.name = `${entry.name}/${entry.children[0].name}`; - entry.children = entry.children[0].children; - } - } + let newParent = { + name: split, + children: [], + isFile, + } as { + name: string, + children: any[], + isFile: boolean, + file?: any, }; - // Merge folders with just a folder as children in order to - // 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() { - // expand file if the selected file is folded - if (this.store.selectedItem) { - const box = document.querySelector(this.store.selectedItem); - const folded = box?.getAttribute('data-folded') === 'true'; - if (folded) setFileFolding(box, box.querySelector('.fold-file'), false); + + if (isFile === true) { + newParent.file = file; + } + + if (parent) { + // check if the folder already exists + const existingFolder = parent.children.find( + (x) => x.name === split, + ); + if (existingFolder) { + newParent = existingFolder; + } else { + parent.children.push(newParent); + } + } else { + const existingFolder = result.find((x) => x.name === split); + if (existingFolder) { + newParent = existingFolder; + } else { + result.push(newParent); + } + } + parent = newParent; + } + } + const mergeChildIfOnlyOneDir = (entries) => { + for (const entry of entries) { + if (entry.children) { + mergeChildIfOnlyOneDir(entry.children); } - }, - 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) { - const btn = document.querySelector('.diff-toggle-file-tree-button'); - const [toShow, toHide] = btn.querySelectorAll('.icon'); - const tree = document.querySelector('#diff-file-tree'); - const newTooltip = btn.getAttribute(visible ? 'data-hide-text' : 'data-show-text'); - btn.setAttribute('data-tooltip-content', newTooltip); - toggleElem(tree, visible); - toggleElem(toShow, !visible); - toggleElem(toHide, visible); - }, - loadMoreData() { - loadMoreFiles(this.store.linkLoadMore); - }, - }, -}; + if (entry.children.length === 1 && entry.children[0].isFile === false) { + // Merge it to the parent + entry.name = `${entry.name}/${entry.children[0].name}`; + entry.children = entry.children[0].children; + } + } + }; + // Merge folders with just a folder as children in order to + // reduce the depth of our tree. + mergeChildIfOnlyOneDir(result); + return result; +}); + +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 (store.selectedItem) { + const box = document.querySelector(store.selectedItem); + const folded = box?.getAttribute('data-folded') === 'true'; + if (folded) setFileFolding(box, box.querySelector('.fold-file'), false); + } +} + +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'); + const newTooltip = btn.getAttribute(visible ? 'data-hide-text' : 'data-show-text'); + btn.setAttribute('data-tooltip-content', newTooltip); + toggleElem(tree, visible); + toggleElem(toShow, !visible); + toggleElem(toHide, visible); +} + +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; diff --git a/web_src/js/components/DiffFileTreeItem.vue b/web_src/js/components/DiffFileTreeItem.vue index d5293af519..84431ff372 100644 --- a/web_src/js/components/DiffFileTreeItem.vue +++ b/web_src/js/components/DiffFileTreeItem.vue @@ -1,33 +1,41 @@ -<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) { - const diffTypes = { - 1: {name: 'octicon-diff-added', classes: ['text', 'green']}, - 2: {name: 'octicon-diff-modified', classes: ['text', 'yellow']}, - 3: {name: 'octicon-diff-removed', classes: ['text', 'red']}, - 4: {name: 'octicon-diff-renamed', classes: ['text', 'teal']}, - 5: {name: 'octicon-diff-renamed', classes: ['text', 'green']}, // there is no octicon for copied, so renamed should be ok - }; - return diffTypes[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']}, + 3: {name: 'octicon-diff-removed', classes: ['text', 'red']}, + 4: {name: 'octicon-diff-renamed', classes: ['text', 'teal']}, + 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 diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue index fc9541b6a6..e8bcee70db 100644 --- a/web_src/js/components/PullRequestMergeForm.vue +++ b/web_src/js/components/PullRequestMergeForm.vue @@ -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, - - mergeTitleFieldValue: '', - mergeMessageFieldValue: '', - deleteBranchAfterMerge: false, - autoMergeWhenSucceed: false, - - mergeStyle: '', - mergeStyleDetail: { // dummy only, these values will come from one of the mergeForm.mergeStyles - 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); - 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; - 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; - }, - }, -}; +const mergeForm = ref(pageData.pullRequestMergeForm); + +const mergeTitleFieldValue = ref(''); +const mergeMessageFieldValue = ref(''); +const deleteBranchAfterMerge = ref(false); +const autoMergeWhenSucceed = ref(false); + +const mergeStyle = ref(''); +const mergeStyleDetail = ref({ + hideMergeMessageTexts: false, + textDoMerge: '', + mergeTitleFieldText: '', + mergeMessageFieldText: '', + hideAutoMerge: false, +}); + +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); + } +}); + +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; + 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 { diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue index c03795abba..054bf6126e 100644 --- a/web_src/js/components/RepoActivityTopAuthors.vue +++ b/web_src/js/components/RepoActivityTopAuthors.vue @@ -1,68 +1,62 @@ -<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: { - barColor: 'green', - textColor: 'black', - textAltColor: 'white', - }, +const colors = ref({ + barColor: 'green', + textColor: 'black', + textAltColor: 'white', +}); - // possible keys: - // * avatar_link: (...) - // * commits: (...) - // * home_link: (...) - // * login: (...) - // * name: (...) - activityTopAuthors: window.config.pageData.repoActivityTopAuthors || [], - }), - computed: { - graphPoints() { - return this.activityTopAuthors.map((item) => { - return { - value: item.commits, - label: item.name, - }; - }); - }, - graphAuthors() { - return this.activityTopAuthors.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); +// possible keys: +// * avatar_link: (...) +// * commits: (...) +// * home_link: (...) +// * login: (...) +// * name: (...) +const activityTopAuthors = window.config.pageData.repoActivityTopAuthors || []; - this.colors.barColor = refStyle.backgroundColor; - this.colors.textColor = refStyle.color; - this.colors.textAltColor = refAltStyle.color; - }, -}; +const graphPoints = computed(() => { + return activityTopAuthors.value.map((item) => { + return { + value: item.commits, + label: item.name, + }; + }); +}); -export function initRepoActivityTopAuthorsChart() { - const el = document.querySelector('#repo-activity-top-authors-chart'); - if (el) { - createApp(sfc).mount(el); - } -} +const graphAuthors = computed(() => { + return activityTopAuthors.value.map((item, idx) => { + return { + position: idx + 1, + ...item, + }; + }); +}); -export default sfc; // activate the IDE's Vue plugin +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, + }; +}); </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" diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue index c30b32405d..eaff8ae0af 100644 --- a/web_src/js/components/RepoCodeFrequency.vue +++ b/web_src/js/components/RepoCodeFrequency.vue @@ -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,114 +36,110 @@ Chart.register( Filler, ); -export default { - components: {ChartLine, SvgIcon}, - props: { - locale: { - type: Object, - required: true, - }, - }, - data: () => ({ - isLoading: false, - errorText: '', - repoLink: pageData.repoLink || [], - data: [], - }), - mounted() { - this.fetchGraphData(); - }, - methods: { - async fetchGraphData() { - this.isLoading = true; - try { - let response; - do { - response = await GET(`${this.repoLink}/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); - const start = weekValues[0].week; - const end = firstStartDateAfterDate(new Date()); - const startDays = startDaysBetween(start, end); - this.data = fillEmptyStartDaysWithZeroes(startDays, this.data); - this.errorText = ''; - } else { - this.errorText = response.statusText; - } - } catch (err) { - this.errorText = err.message; - } finally { - this.isLoading = false; +defineProps<{ + locale: { + 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: Response; + do { + 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) { + 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); + data.value = fillEmptyStartDaysWithZeroes(startDays, data.value); + errorText.value = ''; + } else { + errorText.value = response.statusText; + } + } catch (err) { + errorText.value = err.message; + } finally { + isLoading.value = false; + } +} - toGraphData(data) { - return { - datasets: [ - { - data: data.map((i) => ({x: i.week, y: i.additions})), - pointRadius: 0, - pointHitRadius: 0, - fill: true, - label: 'Additions', - backgroundColor: chartJsColors['additions'], - borderWidth: 0, - tension: 0.3, - }, - { - data: data.map((i) => ({x: i.week, y: -i.deletions})), - pointRadius: 0, - pointHitRadius: 0, - fill: true, - label: 'Deletions', - backgroundColor: chartJsColors['deletions'], - borderWidth: 0, - tension: 0.3, - }, - ], - }; - }, +function toGraphData(data) { + return { + datasets: [ + { + data: data.map((i) => ({x: i.week, y: i.additions})), + pointRadius: 0, + pointHitRadius: 0, + fill: true, + label: 'Additions', + backgroundColor: chartJsColors['additions'], + borderWidth: 0, + tension: 0.3, + }, + { + data: data.map((i) => ({x: i.week, y: -i.deletions})), + pointRadius: 0, + pointHitRadius: 0, + fill: true, + label: 'Deletions', + backgroundColor: chartJsColors['deletions'], + borderWidth: 0, + tension: 0.3, + }, + ], + }; +} - getOptions() { - return { - responsive: true, - maintainAspectRatio: false, - animation: true, - plugins: { - legend: { - display: true, - }, - }, - scales: { - x: { - type: 'time', - grid: { - display: false, - }, - time: { - minUnit: 'month', - }, - ticks: { - maxRotation: 0, - maxTicksLimit: 12, - }, - }, - y: { - ticks: { - maxTicksLimit: 6, - }, - }, - }, - }; +const options = { + responsive: true, + maintainAspectRatio: false, + animation: true, + plugins: { + legend: { + display: true, + }, + }, + scales: { + x: { + type: 'time', + grid: { + display: false, + }, + time: { + minUnit: 'month', + }, + ticks: { + maxRotation: 0, + maxTicksLimit: 12, + }, + }, + y: { + ticks: { + maxTicksLimit: 6, + }, }, }, }; </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; diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue index c3515caba3..8d2a14cd2c 100644 --- a/web_src/js/components/RepoRecentCommits.vue +++ b/web_src/js/components/RepoRecentCommits.vue @@ -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,95 +33,91 @@ Chart.register( Tooltip, ); -export default { - components: {Bar, SvgIcon}, - props: { - locale: { - type: Object, - required: true, - }, - }, - data: () => ({ - isLoading: false, - errorText: '', - repoLink: pageData.repoLink || [], - data: [], - }), - mounted() { - this.fetchGraphData(); - }, - methods: { - async fetchGraphData() { - this.isLoading = true; - try { - let response; - do { - response = await GET(`${this.repoLink}/activity/recent-commits/data`); - if (response.status === 202) { - await sleep(1000); // wait for 1 second before retrying - } - } while (response.status === 202); - if (response.ok) { - const data = await response.json(); - 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 = ''; - } else { - this.errorText = response.statusText; - } - } catch (err) { - this.errorText = err.message; - } finally { - this.isLoading = false; +defineProps<{ + locale: { + 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: Response; + do { + response = await GET(`${repoLink.value}/activity/recent-commits/data`); + if (response.status === 202) { + await sleep(1000); // wait for 1 second before retrying } - }, + } while (response.status === 202); + if (response.ok) { + const data = await response.json(); + const start = Object.values(data)[0].week; + const end = firstStartDateAfterDate(new Date()); + const startDays = startDaysBetween(start, end); + data.value = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52); + errorText.value = ''; + } else { + errorText.value = response.statusText; + } + } catch (err) { + errorText.value = err.message; + } finally { + isLoading.value = false; + } +} - toGraphData(data) { - return { - datasets: [ - { - data: data.map((i) => ({x: i.week, y: i.commits})), - label: 'Commits', - backgroundColor: chartJsColors['commits'], - borderWidth: 0, - tension: 0.3, - }, - ], - }; - }, +function toGraphData(data) { + return { + datasets: [ + { + data: data.map((i) => ({x: i.week, y: i.commits})), + label: 'Commits', + backgroundColor: chartJsColors['commits'], + borderWidth: 0, + tension: 0.3, + }, + ], + }; +} - getOptions() { - return { - responsive: true, - maintainAspectRatio: false, - animation: true, - scales: { - x: { - type: 'time', - grid: { - display: false, - }, - time: { - minUnit: 'week', - }, - ticks: { - maxRotation: 0, - maxTicksLimit: 52, - }, - }, - y: { - ticks: { - maxTicksLimit: 6, - }, - }, - }, - }; +const options = { + responsive: true, + maintainAspectRatio: false, + animation: true, + scales: { + x: { + type: 'time', + grid: { + display: false, + }, + time: { + minUnit: 'week', + }, + ticks: { + maxRotation: 0, + maxTicksLimit: 52, + }, + }, + y: { + ticks: { + maxTicksLimit: 6, + }, }, }, -}; +} 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> diff --git a/web_src/js/components/ScopedAccessTokenSelector.vue b/web_src/js/components/ScopedAccessTokenSelector.vue index 896c1dbff4..63214d0bf5 100644 --- a/web_src/js/components/ScopedAccessTokenSelector.vue +++ b/web_src/js/components/ScopedAccessTokenSelector.vue @@ -1,78 +1,60 @@ -<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 = [ - 'activitypub', - ]; - if (this.isAdmin) { - categories.push('admin'); - } - categories.push( - 'issue', - 'misc', - 'notification', - 'organization', - 'package', - 'repository', - 'user'); - return categories; - }, - }, +const categories = computed(() => { + const categories = [ + 'activitypub', + ]; + if (props.isAdmin) { + categories.push('admin'); + } + categories.push( + 'issue', + 'misc', + 'notification', + 'organization', + 'package', + '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) { - e.preventDefault(); +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')) { - if (el.value) { - // Hide the error if it was visible from previous attempt. - hideElem(warningEl); - // Submit the form. - document.querySelector('#scoped-access-form').submit(); - // Don't show the warning. - return; - } - } - // no scopes selected, show validation error - showElem(warningEl); - }, - }, -}; - -export default sfc; + const warningEl = document.querySelector('#scoped-access-warning'); + // check that at least one scope has been selected + 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<HTMLFormElement>('#scoped-access-form').submit(); + // Don't show the warning. + return; + } + } + // no scopes selected, show validation error + showElem(warningEl); +} </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"> diff --git a/web_src/js/features/repo-common.ts b/web_src/js/features/repo-common.ts index ac63ef2145..de967ffba0 100644 --- a/web_src/js/features/repo-common.ts +++ b/web_src/js/features/repo-common.ts @@ -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'); diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 13dfe1f3ef..f63d199488 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -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, diff --git a/web_src/js/types.ts b/web_src/js/types.ts index f3ac305162..c38c8bda96 100644 --- a/web_src/js/types.ts +++ b/web_src/js/types.ts @@ -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; + }; +}; diff --git a/web_src/js/utils/time.ts b/web_src/js/utils/time.ts index 5251386230..c661155442 100644 --- a/web_src/js/utils/time.ts +++ b/web_src/js/utils/time.ts @@ -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) { |