aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAnbraten <6918444+anbraten@users.noreply.github.com>2024-10-28 21:15:05 +0100
committerGitHub <noreply@github.com>2024-10-28 20:15:05 +0000
commit348d1d0f322ca57c459acd902f54821d687ca804 (patch)
tree442043faa7de26108abd321a34d646afacf66d0d
parenta920fcfd91b1d77cee8bf1143334cba1582b8c5c (diff)
downloadgitea-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.vue34
-rw-r--r--web_src/js/components/ActivityHeatmap.vue96
-rw-r--r--web_src/js/components/ContextPopup.vue156
-rw-r--r--web_src/js/components/DiffFileList.vue68
-rw-r--r--web_src/js/components/DiffFileTree.vue238
-rw-r--r--web_src/js/components/DiffFileTreeItem.vue58
-rw-r--r--web_src/js/components/PullRequestMergeForm.vue150
-rw-r--r--web_src/js/components/RepoActivityTopAuthors.vue106
-rw-r--r--web_src/js/components/RepoCodeFrequency.vue205
-rw-r--r--web_src/js/components/RepoRecentCommits.vue169
-rw-r--r--web_src/js/components/ScopedAccessTokenSelector.vue116
-rw-r--r--web_src/js/features/repo-common.ts9
-rw-r--r--web_src/js/index.ts3
-rw-r--r--web_src/js/types.ts10
-rw-r--r--web_src/js/utils/time.ts4
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) {