Migrate vue components to setup (#32329)
Some checks are pending
release-nightly / nightly-binary (push) Waiting to run
release-nightly / nightly-docker-rootful (push) Waiting to run
release-nightly / nightly-docker-rootless (push) Waiting to run

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:
Anbraten 2024-10-28 21:15:05 +01:00 committed by GitHub
parent a920fcfd91
commit 348d1d0f32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 702 additions and 708 deletions

View File

@ -2,31 +2,21 @@
Please also update the template file above if this vue is modified. Please also update the template file above if this vue is modified.
action status accepted: success, skipped, waiting, blocked, running, failure, cancelled, unknown action status accepted: success, skipped, waiting, blocked, running, failure, cancelled, unknown
--> -->
<script lang="ts"> <script lang="ts" setup>
import {SvgIcon} from '../svg.ts'; import {SvgIcon} from '../svg.ts';
export default { withDefaults(defineProps<{
components: {SvgIcon}, status: '',
props: { size?: number,
status: { className?: string,
type: String, localeStatus?: string,
required: true, }>(), {
}, size: 16,
size: { className: undefined,
type: Number, localeStatus: undefined,
default: 16, });
},
className: {
type: String,
default: '',
},
localeStatus: {
type: String,
default: '',
},
},
};
</script> </script>
<template> <template>
<span class="tw-flex tw-items-center" :data-tooltip-content="localeStatus" v-if="status"> <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'"/> <SvgIcon name="octicon-check-circle-fill" class="text green" :size="size" :class-name="className" v-if="status === 'success'"/>

View File

@ -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 // TODO: Switch to upstream after https://github.com/razorness/vue3-calendar-heatmap/pull/34 is merged
import {CalendarHeatmap} from '@silverwind/vue3-calendar-heatmap'; 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 { defineProps<{
components: {CalendarHeatmap}, values?: HeatmapValue[];
props: {
values: {
type: Array,
default: () => [],
},
locale: { locale: {
type: Object, textTotalContributions: string;
default: () => {}, heatMapLocale: Partial<HeatmapLocale>;
}, noDataText: string;
}, tooltipUnit: string;
data: () => ({ };
colorRange: [ }>();
const colorRange = [
'var(--color-secondary-alpha-60)', 'var(--color-secondary-alpha-60)',
'var(--color-secondary-alpha-60)', 'var(--color-secondary-alpha-60)',
'var(--color-primary-light-4)', 'var(--color-primary-light-4)',
@ -23,22 +22,23 @@ export default {
'var(--color-primary)', 'var(--color-primary)',
'var(--color-primary-dark-2)', 'var(--color-primary-dark-2)',
'var(--color-primary-dark-4)', 'var(--color-primary-dark-4)',
], ];
endDate: new Date(),
}), const endDate = ref(new Date());
mounted() {
onMounted(() => {
// work around issue with first legend color being rendered twice and legend cut off // 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.setAttribute('viewBox', '12 0 80 10');
legend.style.marginRight = '-12px'; legend.style.marginRight = '-12px';
}, });
methods: {
handleDayClick(e) { function handleDayClick(e: Event & {date: Date}) {
// Reset filter if same date is clicked // Reset filter if same date is clicked
const params = new URLSearchParams(document.location.search); const params = new URLSearchParams(document.location.search);
const queryDate = params.get('date'); const queryDate = params.get('date');
// Timezone has to be stripped because toISOString() converts to UTC // 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) { if (queryDate && queryDate === clickedDate) {
params.delete('date'); params.delete('date');
@ -50,9 +50,7 @@ export default {
const newSearch = params.toString(); const newSearch = params.toString();
window.location.search = newSearch.length ? `?${newSearch}` : ''; window.location.search = newSearch.length ? `?${newSearch}` : '';
}, }
},
};
</script> </script>
<template> <template>
<div class="total-contributions"> <div class="total-contributions">

View File

@ -1,100 +1,96 @@
<script lang="ts"> <script lang="ts" setup>
import {SvgIcon} from '../svg.ts'; import {SvgIcon} from '../svg.ts';
import {GET} from '../modules/fetch.ts'; import {GET} from '../modules/fetch.ts';
import {computed, onMounted, ref} from 'vue';
import type {Issue} from '../types';
const {appSubUrl, i18n} = window.config; const {appSubUrl, i18n} = window.config;
export default { const loading = ref(false);
components: {SvgIcon}, const issue = ref(null);
data: () => ({ const renderedLabels = ref('');
loading: false, const i18nErrorOccurred = i18n.error_occurred;
issue: null, const i18nErrorMessage = ref(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'});
},
body() { const createdAt = computed(() => new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'}));
const body = this.issue.body.replace(/\n+/g, ' '); const body = computed(() => {
const body = issue.value.body.replace(/\n+/g, ' ');
if (body.length > 85) { if (body.length > 85) {
return `${body.substring(0, 85)}`; return `${body.substring(0, 85)}`;
} }
return body; return body;
}, });
icon() { function getIssueIcon(issue: Issue) {
if (this.issue.pull_request !== null) { if (issue.pull_request) {
if (this.issue.state === 'open') { if (issue.state === 'open') {
if (this.issue.pull_request.draft === true) { if (issue.pull_request.draft === true) {
return 'octicon-git-pull-request-draft'; // WIP PR return 'octicon-git-pull-request-draft'; // WIP PR
} }
return 'octicon-git-pull-request'; // Open 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-merge'; // Merged PR
} }
return 'octicon-git-pull-request'; // Closed 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-opened'; // Open Issue
} }
return 'octicon-issue-closed'; // Closed Issue return 'octicon-issue-closed'; // Closed Issue
}, }
color() { function getIssueColor(issue: Issue) {
if (this.issue.pull_request !== null) { if (issue.pull_request) {
if (this.issue.pull_request.draft === true) { if (issue.pull_request.draft === true) {
return 'grey'; // WIP PR return 'grey'; // WIP PR
} else if (this.issue.pull_request.merged === true) { } else if (issue.pull_request.merged === true) {
return 'purple'; // Merged PR return 'purple'; // Merged PR
} }
} }
if (this.issue.state === 'open') { if (issue.state === 'open') {
return 'green'; // Open Issue return 'green'; // Open Issue
} }
return 'red'; // Closed Issue return 'red'; // Closed Issue
}, }
},
mounted() { const root = ref<HTMLElement | null>(null);
this.$refs.root.addEventListener('ce-load-context-popup', (e) => {
onMounted(() => {
root.value.addEventListener('ce-load-context-popup', (e: CustomEvent) => {
const data = e.detail; const data = e.detail;
if (!this.loading && this.issue === null) { if (!loading.value && issue.value === null) {
this.load(data); load(data);
} }
}); });
}, });
methods: {
async load(data) { async function load(data) {
this.loading = true; loading.value = true;
this.i18nErrorMessage = null; i18nErrorMessage.value = null;
try { try {
const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`); // backend: GetIssueInfo const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`); // backend: GetIssueInfo
const respJson = await response.json(); const respJson = await response.json();
if (!response.ok) { if (!response.ok) {
this.i18nErrorMessage = respJson.message ?? i18n.network_error; i18nErrorMessage.value = respJson.message ?? i18n.network_error;
return; return;
} }
this.issue = respJson.convertedIssue; issue.value = respJson.convertedIssue;
this.renderedLabels = respJson.renderedLabels; renderedLabels.value = respJson.renderedLabels;
} catch { } catch {
this.i18nErrorMessage = i18n.network_error; i18nErrorMessage.value = i18n.network_error;
} finally { } finally {
this.loading = false; loading.value = false;
} }
}, }
},
};
</script> </script>
<template> <template>
<div ref="root"> <div ref="root">
<div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/> <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 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="tw-text-12">{{ issue.repository.full_name }} on {{ createdAt }}</div>
<div class="flex-text-block"> <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"> <span class="issue-title tw-font-semibold tw-break-anywhere">
{{ issue.title }} {{ issue.title }}
<span class="index">#{{ issue.number }}</span> <span class="index">#{{ issue.number }}</span>

View File

@ -1,22 +1,23 @@
<script lang="ts"> <script lang="ts" setup>
import {onMounted, onUnmounted} from 'vue';
import {loadMoreFiles} from '../features/repo-diff.ts'; import {loadMoreFiles} from '../features/repo-diff.ts';
import {diffTreeStore} from '../modules/stores.ts'; import {diffTreeStore} from '../modules/stores.ts';
export default { const store = diffTreeStore();
data: () => {
return {store: diffTreeStore()}; onMounted(() => {
}, document.querySelector('#show-file-list-btn').addEventListener('click', toggleFileList);
mounted() { });
document.querySelector('#show-file-list-btn').addEventListener('click', this.toggleFileList);
}, onUnmounted(() => {
unmounted() { document.querySelector('#show-file-list-btn').removeEventListener('click', toggleFileList);
document.querySelector('#show-file-list-btn').removeEventListener('click', this.toggleFileList); });
},
methods: { function toggleFileList() {
toggleFileList() { store.fileListIsVisible = !store.fileListIsVisible;
this.store.fileListIsVisible = !this.store.fileListIsVisible; }
},
diffTypeToString(pType) { function diffTypeToString(pType) {
const diffTypes = { const diffTypes = {
1: 'add', 1: 'add',
2: 'modify', 2: 'modify',
@ -25,16 +26,17 @@ export default {
5: 'copy', 5: 'copy',
}; };
return diffTypes[pType]; return diffTypes[pType];
}, }
diffStatsWidth(adds, dels) {
function diffStatsWidth(adds, dels) {
return `${adds / (adds + dels) * 100}%`; return `${adds / (adds + dels) * 100}%`;
}, }
loadMoreData() {
loadMoreFiles(this.store.linkLoadMore); function loadMoreData() {
}, loadMoreFiles(store.linkLoadMore);
}, }
};
</script> </script>
<template> <template>
<ol class="diff-stats tw-m-0" ref="root" v-if="store.fileListIsVisible"> <ol class="diff-stats tw-m-0" ref="root" v-if="store.fileListIsVisible">
<li v-for="file in store.files" :key="file.NameHash"> <li v-for="file in store.files" :key="file.NameHash">

View File

@ -1,21 +1,18 @@
<script lang="ts"> <script lang="ts" setup>
import DiffFileTreeItem from './DiffFileTreeItem.vue'; import DiffFileTreeItem from './DiffFileTreeItem.vue';
import {loadMoreFiles} from '../features/repo-diff.ts'; import {loadMoreFiles} from '../features/repo-diff.ts';
import {toggleElem} from '../utils/dom.ts'; import {toggleElem} from '../utils/dom.ts';
import {diffTreeStore} from '../modules/stores.ts'; import {diffTreeStore} from '../modules/stores.ts';
import {setFileFolding} from '../features/file-fold.ts'; import {setFileFolding} from '../features/file-fold.ts';
import {computed, onMounted, onUnmounted} from 'vue';
const LOCAL_STORAGE_KEY = 'diff_file_tree_visible'; const LOCAL_STORAGE_KEY = 'diff_file_tree_visible';
export default { const store = diffTreeStore();
components: {DiffFileTreeItem},
data: () => { const fileTree = computed(() => {
return {store: diffTreeStore()};
},
computed: {
fileTree() {
const result = []; const result = [];
for (const file of this.store.files) { for (const file of store.files) {
// Split file into directories // Split file into directories
const splits = file.Name.split('/'); const splits = file.Name.split('/');
let index = 0; let index = 0;
@ -31,6 +28,11 @@ export default {
name: split, name: split,
children: [], children: [],
isFile, isFile,
} as {
name: string,
children: any[],
isFile: boolean,
file?: any,
}; };
if (isFile === true) { if (isFile === true) {
@ -74,42 +76,47 @@ export default {
// reduce the depth of our tree. // reduce the depth of our tree.
mergeChildIfOnlyOneDir(result); mergeChildIfOnlyOneDir(result);
return 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 = () => { onMounted(() => {
this.store.selectedItem = window.location.hash; // Default to true if unset
this.expandSelectedFile(); store.fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) !== 'false';
}; document.querySelector('.diff-toggle-file-tree-button').addEventListener('click', toggleVisibility);
this.hashChangeListener();
window.addEventListener('hashchange', this.hashChangeListener); hashChangeListener();
}, window.addEventListener('hashchange', hashChangeListener);
unmounted() { });
document.querySelector('.diff-toggle-file-tree-button').removeEventListener('click', this.toggleVisibility);
window.removeEventListener('hashchange', this.hashChangeListener); onUnmounted(() => {
}, document.querySelector('.diff-toggle-file-tree-button').removeEventListener('click', toggleVisibility);
methods: { window.removeEventListener('hashchange', hashChangeListener);
expandSelectedFile() { });
function hashChangeListener() {
store.selectedItem = window.location.hash;
expandSelectedFile();
}
function expandSelectedFile() {
// expand file if the selected file is folded // expand file if the selected file is folded
if (this.store.selectedItem) { if (store.selectedItem) {
const box = document.querySelector(this.store.selectedItem); const box = document.querySelector(store.selectedItem);
const folded = box?.getAttribute('data-folded') === 'true'; const folded = box?.getAttribute('data-folded') === 'true';
if (folded) setFileFolding(box, box.querySelector('.fold-file'), false); if (folded) setFileFolding(box, box.querySelector('.fold-file'), false);
} }
}, }
toggleVisibility() {
this.updateVisibility(!this.store.fileTreeIsVisible); function toggleVisibility() {
}, updateVisibility(!store.fileTreeIsVisible);
updateVisibility(visible) { }
this.store.fileTreeIsVisible = visible;
localStorage.setItem(LOCAL_STORAGE_KEY, this.store.fileTreeIsVisible); function updateVisibility(visible) {
this.updateState(this.store.fileTreeIsVisible); store.fileTreeIsVisible = visible;
}, localStorage.setItem(LOCAL_STORAGE_KEY, store.fileTreeIsVisible);
updateState(visible) { updateState(store.fileTreeIsVisible);
}
function updateState(visible) {
const btn = document.querySelector('.diff-toggle-file-tree-button'); const btn = document.querySelector('.diff-toggle-file-tree-button');
const [toShow, toHide] = btn.querySelectorAll('.icon'); const [toShow, toHide] = btn.querySelectorAll('.icon');
const tree = document.querySelector('#diff-file-tree'); const tree = document.querySelector('#diff-file-tree');
@ -118,13 +125,13 @@ export default {
toggleElem(tree, visible); toggleElem(tree, visible);
toggleElem(toShow, !visible); toggleElem(toShow, !visible);
toggleElem(toHide, visible); toggleElem(toHide, visible);
}, }
loadMoreData() {
loadMoreFiles(this.store.linkLoadMore); function loadMoreData() {
}, loadMoreFiles(store.linkLoadMore);
}, }
};
</script> </script>
<template> <template>
<div v-if="store.fileTreeIsVisible" class="diff-file-tree-items"> <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 --> <!-- 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>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.diff-file-tree-items { .diff-file-tree-items {
display: flex; display: flex;

View File

@ -1,21 +1,30 @@
<script lang="ts"> <script lang="ts" setup>
import {SvgIcon} from '../svg.ts'; import {SvgIcon} from '../svg.ts';
import {diffTreeStore} from '../modules/stores.ts'; import {diffTreeStore} from '../modules/stores.ts';
import {ref} from 'vue';
export default { type File = {
components: {SvgIcon}, Name: string;
props: { NameHash: string;
item: { Type: number;
type: Object, IsViewed: boolean;
required: true, }
},
}, type Item = {
data: () => ({ name: string;
store: diffTreeStore(), isFile: boolean;
collapsed: false, file?: File;
}), children?: Item[];
methods: { };
getIconForDiffType(pType) {
defineProps<{
item: Item,
}>();
const store = diffTreeStore();
const collapsed = ref(false);
function getIconForDiffType(pType) {
const diffTypes = { const diffTypes = {
1: {name: 'octicon-diff-added', classes: ['text', 'green']}, 1: {name: 'octicon-diff-added', classes: ['text', 'green']},
2: {name: 'octicon-diff-modified', classes: ['text', 'yellow']}, 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 5: {name: 'octicon-diff-renamed', classes: ['text', 'green']}, // there is no octicon for copied, so renamed should be ok
}; };
return diffTypes[pType]; return diffTypes[pType];
}, }
},
};
</script> </script>
<template> <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"--> <!--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 <a

View File

@ -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 {SvgIcon} from '../svg.ts';
import {toggleElem} from '../utils/dom.ts'; import {toggleElem} from '../utils/dom.ts';
const {csrfToken, pageData} = window.config; const {csrfToken, pageData} = window.config;
export default { const mergeForm = ref(pageData.pullRequestMergeForm);
components: {SvgIcon},
data: () => ({
csrfToken,
mergeForm: pageData.pullRequestMergeForm,
mergeTitleFieldValue: '', const mergeTitleFieldValue = ref('');
mergeMessageFieldValue: '', const mergeMessageFieldValue = ref('');
deleteBranchAfterMerge: false, const deleteBranchAfterMerge = ref(false);
autoMergeWhenSucceed: false, const autoMergeWhenSucceed = ref(false);
mergeStyle: '', const mergeStyle = ref('');
mergeStyleDetail: { // dummy only, these values will come from one of the mergeForm.mergeStyles const mergeStyleDetail = ref({
hideMergeMessageTexts: false, hideMergeMessageTexts: false,
textDoMerge: '', textDoMerge: '',
mergeTitleFieldText: '', mergeTitleFieldText: '',
mergeMessageFieldText: '', mergeMessageFieldText: '',
hideAutoMerge: false, hideAutoMerge: false,
}, });
mergeStyleAllowedCount: 0,
showMergeStyleMenu: false, const mergeStyleAllowedCount = ref(0);
showActionForm: false,
}), const showMergeStyleMenu = ref(false);
computed: { const showActionForm = ref(false);
mergeButtonStyleClass() {
if (this.mergeForm.allOverridableChecksOk) return 'primary'; const mergeButtonStyleClass = computed(() => {
return this.autoMergeWhenSucceed ? 'primary' : 'red'; if (mergeForm.value.allOverridableChecksOk) return 'primary';
}, return autoMergeWhenSucceed.value ? 'primary' : 'red';
forceMerge() { });
return this.mergeForm.canMergeNow && !this.mergeForm.allOverridableChecksOk;
}, const forceMerge = computed(() => {
}, return mergeForm.value.canMergeNow && !mergeForm.value.allOverridableChecksOk;
watch: { });
mergeStyle(val) {
this.mergeStyleDetail = this.mergeForm.mergeStyles.find((e) => e.name === val); watch(mergeStyle, (val) => {
mergeStyleDetail.value = mergeForm.value.mergeStyles.find((e) => e.name === val);
for (const elem of document.querySelectorAll('[data-pull-merge-style]')) { for (const elem of document.querySelectorAll('[data-pull-merge-style]')) {
toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val); 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; onMounted(() => {
if (!mergeStyle) mergeStyle = this.mergeForm.mergeStyles.find((e) => e.allowed)?.name; mergeStyleAllowedCount.value = mergeForm.value.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0);
this.switchMergeStyle(mergeStyle, !this.mergeForm.canMergeNow);
}, let mergeStyle = mergeForm.value.mergeStyles.find((e) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name;
mounted() { if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e) => e.allowed)?.name;
document.addEventListener('mouseup', this.hideMergeStyleMenu); switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow);
},
unmounted() { document.addEventListener('mouseup', hideMergeStyleMenu);
document.removeEventListener('mouseup', this.hideMergeStyleMenu); });
},
methods: { onUnmounted(() => {
hideMergeStyleMenu() { document.removeEventListener('mouseup', hideMergeStyleMenu);
this.showMergeStyleMenu = false; });
},
toggleActionForm(show) { function hideMergeStyleMenu() {
this.showActionForm = show; showMergeStyleMenu.value = false;
}
function toggleActionForm(show: boolean) {
showActionForm.value = show;
if (!show) return; if (!show) return;
this.deleteBranchAfterMerge = this.mergeForm.defaultDeleteBranchAfterMerge; deleteBranchAfterMerge.value = mergeForm.value.defaultDeleteBranchAfterMerge;
this.mergeTitleFieldValue = this.mergeStyleDetail.mergeTitleFieldText; mergeTitleFieldValue.value = mergeStyleDetail.value.mergeTitleFieldText;
this.mergeMessageFieldValue = this.mergeStyleDetail.mergeMessageFieldText; mergeMessageFieldValue.value = mergeStyleDetail.value.mergeMessageFieldText;
}, }
switchMergeStyle(name, autoMerge = false) {
this.mergeStyle = name; function switchMergeStyle(name, autoMerge = false) {
this.autoMergeWhenSucceed = autoMerge; mergeStyle.value = name;
}, autoMergeWhenSucceed.value = autoMerge;
clearMergeMessage() { }
this.mergeMessageFieldValue = this.mergeForm.defaultMergeMessage;
}, function clearMergeMessage() {
}, mergeMessageFieldValue.value = mergeForm.value.defaultMergeMessage;
}; }
</script> </script>
<template> <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 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>
</div> </div>
</template> </template>
<style scoped> <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 */ /* 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 { .ui.dropdown .menu.show {

View File

@ -1,68 +1,62 @@
<script lang="ts"> <script lang="ts" setup>
import {VueBarGraph} from 'vue-bar-graph'; import {VueBarGraph} from 'vue-bar-graph';
import {createApp} from 'vue'; import {computed, onMounted, ref} from 'vue';
const sfc = { const colors = ref({
components: {VueBarGraph},
data: () => ({
colors: {
barColor: 'green', barColor: 'green',
textColor: 'black', textColor: 'black',
textAltColor: 'white', textAltColor: 'white',
}, });
// possible keys: // possible keys:
// * avatar_link: (...) // * avatar_link: (...)
// * commits: (...) // * commits: (...)
// * home_link: (...) // * home_link: (...)
// * login: (...) // * login: (...)
// * name: (...) // * name: (...)
activityTopAuthors: window.config.pageData.repoActivityTopAuthors || [], const activityTopAuthors = window.config.pageData.repoActivityTopAuthors || [];
}),
computed: { const graphPoints = computed(() => {
graphPoints() { return activityTopAuthors.value.map((item) => {
return this.activityTopAuthors.map((item) => {
return { return {
value: item.commits, value: item.commits,
label: item.name, label: item.name,
}; };
}); });
}, });
graphAuthors() {
return this.activityTopAuthors.map((item, idx) => { const graphAuthors = computed(() => {
return activityTopAuthors.value.map((item, idx) => {
return { return {
position: idx + 1, position: idx + 1,
...item, ...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; const graphWidth = computed(() => {
this.colors.textColor = refStyle.color; return activityTopAuthors.value.length * 40;
this.colors.textAltColor = refAltStyle.color; });
},
};
export function initRepoActivityTopAuthorsChart() { const styleElement = ref<HTMLElement | null>(null);
const el = document.querySelector('#repo-activity-top-authors-chart'); const altStyleElement = ref<HTMLElement | null>(null);
if (el) {
createApp(sfc).mount(el);
}
}
export default sfc; // activate the IDE's Vue plugin 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> </script>
<template> <template>
<div> <div>
<div class="activity-bar-graph" ref="style" style="width: 0; height: 0;"/> <div class="activity-bar-graph" ref="styleElement" style="width: 0; height: 0;"/>
<div class="activity-bar-graph-alt" ref="altStyle" style="width: 0; height: 0;"/> <div class="activity-bar-graph-alt" ref="altStyleElement" style="width: 0; height: 0;"/>
<vue-bar-graph <vue-bar-graph
:points="graphPoints" :points="graphPoints"
:show-x-axis="true" :show-x-axis="true"

View File

@ -1,4 +1,4 @@
<script lang="ts"> <script lang="ts" setup>
import {SvgIcon} from '../svg.ts'; import {SvgIcon} from '../svg.ts';
import { import {
Chart, Chart,
@ -15,10 +15,12 @@ import {
startDaysBetween, startDaysBetween,
firstStartDateAfterDate, firstStartDateAfterDate,
fillEmptyStartDaysWithZeroes, fillEmptyStartDaysWithZeroes,
type DayData,
} from '../utils/time.ts'; } from '../utils/time.ts';
import {chartJsColors} from '../utils/color.ts'; import {chartJsColors} from '../utils/color.ts';
import {sleep} from '../utils.ts'; import {sleep} from '../utils.ts';
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
import {onMounted, ref} from 'vue';
const {pageData} = window.config; const {pageData} = window.config;
@ -34,53 +36,52 @@ Chart.register(
Filler, Filler,
); );
export default { defineProps<{
components: {ChartLine, SvgIcon},
props: {
locale: { locale: {
type: Object, loadingTitle: string;
required: true, loadingTitleFailed: string;
}, loadingInfo: string;
}, };
data: () => ({ }>();
isLoading: false,
errorText: '', const isLoading = ref(false);
repoLink: pageData.repoLink || [], const errorText = ref('');
data: [], const repoLink = ref(pageData.repoLink || []);
}), const data = ref<DayData[]>([]);
mounted() {
this.fetchGraphData(); onMounted(() => {
}, fetchGraphData();
methods: { });
async fetchGraphData() {
this.isLoading = true; async function fetchGraphData() {
isLoading.value = true;
try { try {
let response; let response: Response;
do { do {
response = await GET(`${this.repoLink}/activity/code-frequency/data`); response = await GET(`${repoLink.value}/activity/code-frequency/data`);
if (response.status === 202) { if (response.status === 202) {
await sleep(1000); // wait for 1 second before retrying await sleep(1000); // wait for 1 second before retrying
} }
} while (response.status === 202); } while (response.status === 202);
if (response.ok) { if (response.ok) {
this.data = await response.json(); data.value = await response.json();
const weekValues = Object.values(this.data); const weekValues = Object.values(data.value);
const start = weekValues[0].week; const start = weekValues[0].week;
const end = firstStartDateAfterDate(new Date()); const end = firstStartDateAfterDate(new Date());
const startDays = startDaysBetween(start, end); const startDays = startDaysBetween(start, end);
this.data = fillEmptyStartDaysWithZeroes(startDays, this.data); data.value = fillEmptyStartDaysWithZeroes(startDays, data.value);
this.errorText = ''; errorText.value = '';
} else { } else {
this.errorText = response.statusText; errorText.value = response.statusText;
} }
} catch (err) { } catch (err) {
this.errorText = err.message; errorText.value = err.message;
} finally { } finally {
this.isLoading = false; isLoading.value = false;
} }
}, }
toGraphData(data) { function toGraphData(data) {
return { return {
datasets: [ datasets: [
{ {
@ -105,10 +106,9 @@ export default {
}, },
], ],
}; };
}, }
getOptions() { const options = {
return {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
animation: true, animation: true,
@ -137,11 +137,9 @@ export default {
}, },
}, },
}, },
};
},
},
}; };
</script> </script>
<template> <template>
<div> <div>
<div class="ui header tw-flex tw-items-center tw-justify-between"> <div class="ui header tw-flex tw-items-center tw-justify-between">
@ -160,11 +158,12 @@ export default {
</div> </div>
<ChartLine <ChartLine
v-memo="data" v-if="data.length !== 0" v-memo="data" v-if="data.length !== 0"
:data="toGraphData(data)" :options="getOptions()" :data="toGraphData(data)" :options="options"
/> />
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.main-graph { .main-graph {
height: 440px; height: 440px;

View File

@ -1,4 +1,4 @@
<script lang="ts"> <script lang="ts" setup>
import {SvgIcon} from '../svg.ts'; import {SvgIcon} from '../svg.ts';
import { import {
Chart, Chart,
@ -6,6 +6,7 @@ import {
BarElement, BarElement,
LinearScale, LinearScale,
TimeScale, TimeScale,
type ChartOptions,
} from 'chart.js'; } from 'chart.js';
import {GET} from '../modules/fetch.ts'; import {GET} from '../modules/fetch.ts';
import {Bar} from 'vue-chartjs'; import {Bar} from 'vue-chartjs';
@ -13,10 +14,12 @@ import {
startDaysBetween, startDaysBetween,
firstStartDateAfterDate, firstStartDateAfterDate,
fillEmptyStartDaysWithZeroes, fillEmptyStartDaysWithZeroes,
type DayData,
} from '../utils/time.ts'; } from '../utils/time.ts';
import {chartJsColors} from '../utils/color.ts'; import {chartJsColors} from '../utils/color.ts';
import {sleep} from '../utils.ts'; import {sleep} from '../utils.ts';
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
import {onMounted, ref} from 'vue';
const {pageData} = window.config; const {pageData} = window.config;
@ -30,30 +33,29 @@ Chart.register(
Tooltip, Tooltip,
); );
export default { defineProps<{
components: {Bar, SvgIcon},
props: {
locale: { locale: {
type: Object, loadingTitle: string;
required: true, loadingTitleFailed: string;
}, loadingInfo: string;
}, };
data: () => ({ }>();
isLoading: false,
errorText: '', const isLoading = ref(false);
repoLink: pageData.repoLink || [], const errorText = ref('');
data: [], const repoLink = ref(pageData.repoLink || []);
}), const data = ref<DayData[]>([]);
mounted() {
this.fetchGraphData(); onMounted(() => {
}, fetchGraphData();
methods: { });
async fetchGraphData() {
this.isLoading = true; async function fetchGraphData() {
isLoading.value = true;
try { try {
let response; let response: Response;
do { do {
response = await GET(`${this.repoLink}/activity/recent-commits/data`); response = await GET(`${repoLink.value}/activity/recent-commits/data`);
if (response.status === 202) { if (response.status === 202) {
await sleep(1000); // wait for 1 second before retrying await sleep(1000); // wait for 1 second before retrying
} }
@ -63,19 +65,19 @@ export default {
const start = Object.values(data)[0].week; const start = Object.values(data)[0].week;
const end = firstStartDateAfterDate(new Date()); const end = firstStartDateAfterDate(new Date());
const startDays = startDaysBetween(start, end); const startDays = startDaysBetween(start, end);
this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52); data.value = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52);
this.errorText = ''; errorText.value = '';
} else { } else {
this.errorText = response.statusText; errorText.value = response.statusText;
} }
} catch (err) { } catch (err) {
this.errorText = err.message; errorText.value = err.message;
} finally { } finally {
this.isLoading = false; isLoading.value = false;
} }
}, }
toGraphData(data) { function toGraphData(data) {
return { return {
datasets: [ datasets: [
{ {
@ -87,10 +89,9 @@ export default {
}, },
], ],
}; };
}, }
getOptions() { const options = {
return {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
animation: true, animation: true,
@ -114,11 +115,9 @@ export default {
}, },
}, },
}, },
}; } satisfies ChartOptions;
},
},
};
</script> </script>
<template> <template>
<div> <div>
<div class="ui header tw-flex tw-items-center tw-justify-between"> <div class="ui header tw-flex tw-items-center tw-justify-between">
@ -137,7 +136,7 @@ export default {
</div> </div>
<Bar <Bar
v-memo="data" v-if="data.length !== 0" v-memo="data" v-if="data.length !== 0"
:data="toGraphData(data)" :options="getOptions()" :data="toGraphData(data)" :options="options"
/> />
</div> </div>
</div> </div>

View File

@ -1,32 +1,19 @@
<script lang="ts"> <script lang="ts" setup>
import {computed, onMounted, onUnmounted} from 'vue';
import {hideElem, showElem} from '../utils/dom.ts'; import {hideElem, showElem} from '../utils/dom.ts';
const sfc = { const props = defineProps<{
props: { isAdmin: boolean;
isAdmin: { noAccessLabel: string;
type: Boolean, readLabel: string;
required: true, writeLabel: string;
}, }>();
noAccessLabel: {
type: String,
required: true,
},
readLabel: {
type: String,
required: true,
},
writeLabel: {
type: String,
required: true,
},
},
computed: { const categories = computed(() => {
categories() {
const categories = [ const categories = [
'activitypub', 'activitypub',
]; ];
if (this.isAdmin) { if (props.isAdmin) {
categories.push('admin'); categories.push('admin');
} }
categories.push( categories.push(
@ -38,41 +25,36 @@ const sfc = {
'repository', 'repository',
'user'); 'user');
return categories; return categories;
}, });
},
mounted() { onMounted(() => {
document.querySelector('#scoped-access-submit').addEventListener('click', this.onClickSubmit); document.querySelector('#scoped-access-submit').addEventListener('click', onClickSubmit);
}, });
unmounted() { onUnmounted(() => {
document.querySelector('#scoped-access-submit').removeEventListener('click', this.onClickSubmit); document.querySelector('#scoped-access-submit').removeEventListener('click', onClickSubmit);
}, });
methods: { function onClickSubmit(e) {
onClickSubmit(e) {
e.preventDefault(); e.preventDefault();
const warningEl = document.querySelector('#scoped-access-warning'); const warningEl = document.querySelector('#scoped-access-warning');
// check that at least one scope has been selected // 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) { if (el.value) {
// Hide the error if it was visible from previous attempt. // Hide the error if it was visible from previous attempt.
hideElem(warningEl); hideElem(warningEl);
// Submit the form. // Submit the form.
document.querySelector('#scoped-access-form').submit(); document.querySelector<HTMLFormElement>('#scoped-access-form').submit();
// Don't show the warning. // Don't show the warning.
return; return;
} }
} }
// no scopes selected, show validation error // no scopes selected, show validation error
showElem(warningEl); showElem(warningEl);
}, }
},
};
export default sfc;
</script> </script>
<template> <template>
<div v-for="category in categories" :key="category" class="field tw-pl-1 tw-pb-1 access-token-category"> <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"> <label class="category-label" :for="'access-token-scope-' + category">

View File

@ -3,6 +3,8 @@ import {hideElem, queryElems, showElem} from '../utils/dom.ts';
import {POST} from '../modules/fetch.ts'; import {POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts'; import {showErrorToast} from '../modules/toast.ts';
import {sleep} from '../utils.ts'; import {sleep} from '../utils.ts';
import RepoActivityTopAuthors from '../components/RepoActivityTopAuthors.vue';
import {createApp} from 'vue';
async function onDownloadArchive(e) { async function onDownloadArchive(e) {
e.preventDefault(); e.preventDefault();
@ -32,6 +34,13 @@ export function initRepoArchiveLinks() {
queryElems('a.archive-link[href]', (el) => el.addEventListener('click', onDownloadArchive)); 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() { export function initRepoCloneLink() {
const $repoCloneSsh = $('#repo-clone-ssh'); const $repoCloneSsh = $('#repo-clone-ssh');
const $repoCloneHttps = $('#repo-clone-https'); const $repoCloneHttps = $('#repo-clone-https');

View File

@ -2,7 +2,6 @@
import './bootstrap.ts'; import './bootstrap.ts';
import './htmx.ts'; import './htmx.ts';
import {initRepoActivityTopAuthorsChart} from './components/RepoActivityTopAuthors.vue';
import {initDashboardRepoList} from './components/DashboardRepoList.vue'; import {initDashboardRepoList} from './components/DashboardRepoList.vue';
import {initGlobalCopyToClipboardListener} from './features/clipboard.ts'; 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 {initRepoCodeView} from './features/repo-code.ts';
import {initSshKeyFormParser} from './features/sshkey-helper.ts'; import {initSshKeyFormParser} from './features/sshkey-helper.ts';
import {initUserSettings} from './features/user-settings.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 {initRepoMigrationStatusChecker} from './features/repo-migrate.ts';
import { import {
initRepoSettingGitHook, initRepoSettingGitHook,

View File

@ -36,3 +36,13 @@ export type IssueData = {
type: string, type: string,
index: string, index: string,
} }
export type Issue = {
id: number;
title: string;
state: 'open' | 'closed';
pull_request?: {
draft: boolean;
merged: boolean;
};
};

View File

@ -42,14 +42,14 @@ export function firstStartDateAfterDate(inputDate: Date): number {
return resultDate.valueOf(); return resultDate.valueOf();
} }
type DayData = { export type DayData = {
week: number, week: number,
additions: number, additions: number,
deletions: number, deletions: number,
commits: number, commits: number,
} }
export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayData): DayData[] { export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayData[]): DayData[] {
const result = {}; const result = {};
for (const startDay of startDays) { for (const startDay of startDays) {