mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 12:26:05 +01:00 
			
		
		
		
	Migrate vue components to setup (#32329)
Migrated a handful Vue components to the `setup` syntax using composition api as it has better Typescript support and is becoming the new default in the Vue ecosystem. - [x] ActionRunStatus.vue - [x] ActivityHeatmap.vue - [x] ContextPopup.vue - [x] DiffFileList.vue - [x] DiffFileTree.vue - [x] DiffFileTreeItem.vue - [x] PullRequestMergeForm.vue - [x] RepoActivityTopAuthors.vue - [x] RepoCodeFrequency.vue - [x] RepoRecentCommits.vue - [x] ScopedAccessTokenSelector.vue Left some larger components untouched for now to not go to crazy in this single PR: - [ ] DiffCommitSelector.vue - [ ] RepoActionView.vue - [ ] RepoContributors.vue - [ ] DashboardRepoList.vue - [ ] RepoBranchTagSelector.vue
This commit is contained in:
		@@ -2,31 +2,21 @@
 | 
				
			|||||||
    Please also update the template file above if this vue is modified.
 | 
					    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'"/>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
					// 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: {
 | 
					  locale: {
 | 
				
			||||||
    values: {
 | 
					    textTotalContributions: string;
 | 
				
			||||||
      type: Array,
 | 
					    heatMapLocale: Partial<HeatmapLocale>;
 | 
				
			||||||
      default: () => [],
 | 
					    noDataText: string;
 | 
				
			||||||
    },
 | 
					    tooltipUnit: string;
 | 
				
			||||||
    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);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (queryDate && queryDate === clickedDate) {
 | 
					const colorRange = [
 | 
				
			||||||
        params.delete('date');
 | 
					  'var(--color-secondary-alpha-60)',
 | 
				
			||||||
      } else {
 | 
					  'var(--color-secondary-alpha-60)',
 | 
				
			||||||
        params.set('date', clickedDate);
 | 
					  '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();
 | 
					onMounted(() => {
 | 
				
			||||||
      window.location.search = newSearch.length ? `?${newSearch}` : '';
 | 
					  // 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>
 | 
					</script>
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="total-contributions">
 | 
					  <div class="total-contributions">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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(() => {
 | 
				
			||||||
      if (body.length > 85) {
 | 
					  const body = issue.value.body.replace(/\n+/g, ' ');
 | 
				
			||||||
        return `${body.substring(0, 85)}…`;
 | 
					  if (body.length > 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
 | 
					 | 
				
			||||||
        } else if (this.issue.pull_request.merged === true) {
 | 
					 | 
				
			||||||
          return 'octicon-git-merge'; // Merged PR
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return 'octicon-git-pull-request'; // Closed PR
 | 
					 | 
				
			||||||
      } else if (this.issue.state === 'open') {
 | 
					 | 
				
			||||||
        return 'octicon-issue-opened'; // Open Issue
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return 'octicon-issue-closed'; // Closed Issue
 | 
					      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() {
 | 
					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() {
 | 
					 | 
				
			||||||
    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;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      try {
 | 
					const root = ref<HTMLElement | null>(null);
 | 
				
			||||||
        const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`); // backend: GetIssueInfo
 | 
					
 | 
				
			||||||
        const respJson = await response.json();
 | 
					onMounted(() => {
 | 
				
			||||||
        if (!response.ok) {
 | 
					  root.value.addEventListener('ce-load-context-popup', (e: CustomEvent) => {
 | 
				
			||||||
          this.i18nErrorMessage = respJson.message ?? i18n.network_error;
 | 
					    const data = e.detail;
 | 
				
			||||||
          return;
 | 
					    if (!loading.value && issue.value === null) {
 | 
				
			||||||
        }
 | 
					      load(data);
 | 
				
			||||||
        this.issue = respJson.convertedIssue;
 | 
					    }
 | 
				
			||||||
        this.renderedLabels = respJson.renderedLabels;
 | 
					  });
 | 
				
			||||||
      } catch {
 | 
					});
 | 
				
			||||||
        this.i18nErrorMessage = i18n.network_error;
 | 
					
 | 
				
			||||||
      } finally {
 | 
					async function load(data) {
 | 
				
			||||||
        this.loading = false;
 | 
					  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>
 | 
					</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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,40 +1,42 @@
 | 
				
			|||||||
<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',
 | 
				
			||||||
        3: 'del',
 | 
					    3: 'del',
 | 
				
			||||||
        4: 'rename',
 | 
					    4: 'rename',
 | 
				
			||||||
        5: 'copy',
 | 
					    5: 'copy',
 | 
				
			||||||
      };
 | 
					  };
 | 
				
			||||||
      return diffTypes[pType];
 | 
					  return diffTypes[pType];
 | 
				
			||||||
    },
 | 
					}
 | 
				
			||||||
    diffStatsWidth(adds, dels) {
 | 
					
 | 
				
			||||||
      return `${adds / (adds + dels) * 100}%`;
 | 
					function diffStatsWidth(adds, dels) {
 | 
				
			||||||
    },
 | 
					  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">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,130 +1,137 @@
 | 
				
			|||||||
<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: () => {
 | 
					 | 
				
			||||||
    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) {
 | 
					const fileTree = computed(() => {
 | 
				
			||||||
            newParent.file = file;
 | 
					  const result = [];
 | 
				
			||||||
          }
 | 
					  for (const file of store.files) {
 | 
				
			||||||
 | 
					    // Split file into directories
 | 
				
			||||||
          if (parent) {
 | 
					    const splits = file.Name.split('/');
 | 
				
			||||||
            // check if the folder already exists
 | 
					    let index = 0;
 | 
				
			||||||
            const existingFolder = parent.children.find(
 | 
					    let parent = null;
 | 
				
			||||||
              (x) => x.name === split,
 | 
					    let isFile = false;
 | 
				
			||||||
            );
 | 
					    for (const split of splits) {
 | 
				
			||||||
            if (existingFolder) {
 | 
					      index += 1;
 | 
				
			||||||
              newParent = existingFolder;
 | 
					      // reached the end
 | 
				
			||||||
            } else {
 | 
					      if (index === splits.length) {
 | 
				
			||||||
              parent.children.push(newParent);
 | 
					        isFile = true;
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          } else {
 | 
					 | 
				
			||||||
            const existingFolder = result.find((x) => x.name === split);
 | 
					 | 
				
			||||||
            if (existingFolder) {
 | 
					 | 
				
			||||||
              newParent = existingFolder;
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
              result.push(newParent);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          parent = newParent;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const mergeChildIfOnlyOneDir = (entries) => {
 | 
					      let newParent = {
 | 
				
			||||||
        for (const entry of entries) {
 | 
					        name: split,
 | 
				
			||||||
          if (entry.children) {
 | 
					        children: [],
 | 
				
			||||||
            mergeChildIfOnlyOneDir(entry.children);
 | 
					        isFile,
 | 
				
			||||||
          }
 | 
					      } as {
 | 
				
			||||||
          if (entry.children.length === 1 && entry.children[0].isFile === false) {
 | 
					        name: string,
 | 
				
			||||||
            // Merge it to the parent
 | 
					        children: any[],
 | 
				
			||||||
            entry.name = `${entry.name}/${entry.children[0].name}`;
 | 
					        isFile: boolean,
 | 
				
			||||||
            entry.children = entry.children[0].children;
 | 
					        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 = () => {
 | 
					      if (isFile === true) {
 | 
				
			||||||
      this.store.selectedItem = window.location.hash;
 | 
					        newParent.file = file;
 | 
				
			||||||
      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);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					
 | 
				
			||||||
    toggleVisibility() {
 | 
					      if (parent) {
 | 
				
			||||||
      this.updateVisibility(!this.store.fileTreeIsVisible);
 | 
					        // check if the folder already exists
 | 
				
			||||||
    },
 | 
					        const existingFolder = parent.children.find(
 | 
				
			||||||
    updateVisibility(visible) {
 | 
					          (x) => x.name === split,
 | 
				
			||||||
      this.store.fileTreeIsVisible = visible;
 | 
					        );
 | 
				
			||||||
      localStorage.setItem(LOCAL_STORAGE_KEY, this.store.fileTreeIsVisible);
 | 
					        if (existingFolder) {
 | 
				
			||||||
      this.updateState(this.store.fileTreeIsVisible);
 | 
					          newParent = existingFolder;
 | 
				
			||||||
    },
 | 
					        } else {
 | 
				
			||||||
    updateState(visible) {
 | 
					          parent.children.push(newParent);
 | 
				
			||||||
      const btn = document.querySelector('.diff-toggle-file-tree-button');
 | 
					        }
 | 
				
			||||||
      const [toShow, toHide] = btn.querySelectorAll('.icon');
 | 
					      } else {
 | 
				
			||||||
      const tree = document.querySelector('#diff-file-tree');
 | 
					        const existingFolder = result.find((x) => x.name === split);
 | 
				
			||||||
      const newTooltip = btn.getAttribute(visible ? 'data-hide-text' : 'data-show-text');
 | 
					        if (existingFolder) {
 | 
				
			||||||
      btn.setAttribute('data-tooltip-content', newTooltip);
 | 
					          newParent = existingFolder;
 | 
				
			||||||
      toggleElem(tree, visible);
 | 
					        } else {
 | 
				
			||||||
      toggleElem(toShow, !visible);
 | 
					          result.push(newParent);
 | 
				
			||||||
      toggleElem(toHide, visible);
 | 
					        }
 | 
				
			||||||
    },
 | 
					      }
 | 
				
			||||||
    loadMoreData() {
 | 
					      parent = newParent;
 | 
				
			||||||
      loadMoreFiles(this.store.linkLoadMore);
 | 
					    }
 | 
				
			||||||
    },
 | 
					  }
 | 
				
			||||||
  },
 | 
					  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;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  // 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>
 | 
					</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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,33 +1,41 @@
 | 
				
			|||||||
<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) {
 | 
					 | 
				
			||||||
      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];
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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>
 | 
					</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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
					 | 
				
			||||||
  }),
 | 
					 | 
				
			||||||
  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;
 | 
					const showMergeStyleMenu = ref(false);
 | 
				
			||||||
    if (!mergeStyle) mergeStyle = this.mergeForm.mergeStyles.find((e) => e.allowed)?.name;
 | 
					const showActionForm = ref(false);
 | 
				
			||||||
    this.switchMergeStyle(mergeStyle, !this.mergeForm.canMergeNow);
 | 
					
 | 
				
			||||||
  },
 | 
					const mergeButtonStyleClass = computed(() => {
 | 
				
			||||||
  mounted() {
 | 
					  if (mergeForm.value.allOverridableChecksOk) return 'primary';
 | 
				
			||||||
    document.addEventListener('mouseup', this.hideMergeStyleMenu);
 | 
					  return autoMergeWhenSucceed.value ? 'primary' : 'red';
 | 
				
			||||||
  },
 | 
					});
 | 
				
			||||||
  unmounted() {
 | 
					
 | 
				
			||||||
    document.removeEventListener('mouseup', this.hideMergeStyleMenu);
 | 
					const forceMerge = computed(() => {
 | 
				
			||||||
  },
 | 
					  return mergeForm.value.canMergeNow && !mergeForm.value.allOverridableChecksOk;
 | 
				
			||||||
  methods: {
 | 
					});
 | 
				
			||||||
    hideMergeStyleMenu() {
 | 
					
 | 
				
			||||||
      this.showMergeStyleMenu = false;
 | 
					watch(mergeStyle, (val) => {
 | 
				
			||||||
    },
 | 
					  mergeStyleDetail.value = mergeForm.value.mergeStyles.find((e) => e.name === val);
 | 
				
			||||||
    toggleActionForm(show) {
 | 
					  for (const elem of document.querySelectorAll('[data-pull-merge-style]')) {
 | 
				
			||||||
      this.showActionForm = show;
 | 
					    toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val);
 | 
				
			||||||
      if (!show) return;
 | 
					  }
 | 
				
			||||||
      this.deleteBranchAfterMerge = this.mergeForm.defaultDeleteBranchAfterMerge;
 | 
					});
 | 
				
			||||||
      this.mergeTitleFieldValue = this.mergeStyleDetail.mergeTitleFieldText;
 | 
					
 | 
				
			||||||
      this.mergeMessageFieldValue = this.mergeStyleDetail.mergeMessageFieldText;
 | 
					onMounted(() => {
 | 
				
			||||||
    },
 | 
					  mergeStyleAllowedCount.value = mergeForm.value.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0);
 | 
				
			||||||
    switchMergeStyle(name, autoMerge = false) {
 | 
					
 | 
				
			||||||
      this.mergeStyle = name;
 | 
					  let mergeStyle = mergeForm.value.mergeStyles.find((e) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name;
 | 
				
			||||||
      this.autoMergeWhenSucceed = autoMerge;
 | 
					  if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e) => e.allowed)?.name;
 | 
				
			||||||
    },
 | 
					  switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow);
 | 
				
			||||||
    clearMergeMessage() {
 | 
					
 | 
				
			||||||
      this.mergeMessageFieldValue = this.mergeForm.defaultMergeMessage;
 | 
					  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>
 | 
					</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 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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},
 | 
					  barColor: 'green',
 | 
				
			||||||
  data: () => ({
 | 
					  textColor: 'black',
 | 
				
			||||||
    colors: {
 | 
					  textAltColor: 'white',
 | 
				
			||||||
      barColor: 'green',
 | 
					});
 | 
				
			||||||
      textColor: 'black',
 | 
					 | 
				
			||||||
      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: {
 | 
					 | 
				
			||||||
    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);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.colors.barColor = refStyle.backgroundColor;
 | 
					const graphPoints = computed(() => {
 | 
				
			||||||
    this.colors.textColor = refStyle.color;
 | 
					  return activityTopAuthors.value.map((item) => {
 | 
				
			||||||
    this.colors.textAltColor = refAltStyle.color;
 | 
					    return {
 | 
				
			||||||
  },
 | 
					      value: item.commits,
 | 
				
			||||||
};
 | 
					      label: item.name,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function initRepoActivityTopAuthorsChart() {
 | 
					const graphAuthors = computed(() => {
 | 
				
			||||||
  const el = document.querySelector('#repo-activity-top-authors-chart');
 | 
					  return activityTopAuthors.value.map((item, idx) => {
 | 
				
			||||||
  if (el) {
 | 
					    return {
 | 
				
			||||||
    createApp(sfc).mount(el);
 | 
					      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>
 | 
					</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"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,114 +36,110 @@ Chart.register(
 | 
				
			|||||||
  Filler,
 | 
					  Filler,
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					defineProps<{
 | 
				
			||||||
  components: {ChartLine, SvgIcon},
 | 
					  locale: {
 | 
				
			||||||
  props: {
 | 
					    loadingTitle: string;
 | 
				
			||||||
    locale: {
 | 
					    loadingTitleFailed: string;
 | 
				
			||||||
      type: Object,
 | 
					    loadingInfo: string;
 | 
				
			||||||
      required: true,
 | 
					  };
 | 
				
			||||||
    },
 | 
					}>();
 | 
				
			||||||
  },
 | 
					
 | 
				
			||||||
  data: () => ({
 | 
					const isLoading = ref(false);
 | 
				
			||||||
    isLoading: false,
 | 
					const errorText = ref('');
 | 
				
			||||||
    errorText: '',
 | 
					const repoLink = ref(pageData.repoLink || []);
 | 
				
			||||||
    repoLink: pageData.repoLink || [],
 | 
					const data = ref<DayData[]>([]);
 | 
				
			||||||
    data: [],
 | 
					
 | 
				
			||||||
  }),
 | 
					onMounted(() => {
 | 
				
			||||||
  mounted() {
 | 
					  fetchGraphData();
 | 
				
			||||||
    this.fetchGraphData();
 | 
					});
 | 
				
			||||||
  },
 | 
					
 | 
				
			||||||
  methods: {
 | 
					async function fetchGraphData() {
 | 
				
			||||||
    async fetchGraphData() {
 | 
					  isLoading.value = true;
 | 
				
			||||||
      this.isLoading = true;
 | 
					  try {
 | 
				
			||||||
      try {
 | 
					    let response: Response;
 | 
				
			||||||
        let response;
 | 
					    do {
 | 
				
			||||||
        do {
 | 
					      response = await GET(`${repoLink.value}/activity/code-frequency/data`);
 | 
				
			||||||
          response = await GET(`${this.repoLink}/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);
 | 
					 | 
				
			||||||
        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;
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    } 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) {
 | 
					function toGraphData(data) {
 | 
				
			||||||
      return {
 | 
					  return {
 | 
				
			||||||
        datasets: [
 | 
					    datasets: [
 | 
				
			||||||
          {
 | 
					      {
 | 
				
			||||||
            data: data.map((i) => ({x: i.week, y: i.additions})),
 | 
					        data: data.map((i) => ({x: i.week, y: i.additions})),
 | 
				
			||||||
            pointRadius: 0,
 | 
					        pointRadius: 0,
 | 
				
			||||||
            pointHitRadius: 0,
 | 
					        pointHitRadius: 0,
 | 
				
			||||||
            fill: true,
 | 
					        fill: true,
 | 
				
			||||||
            label: 'Additions',
 | 
					        label: 'Additions',
 | 
				
			||||||
            backgroundColor: chartJsColors['additions'],
 | 
					        backgroundColor: chartJsColors['additions'],
 | 
				
			||||||
            borderWidth: 0,
 | 
					        borderWidth: 0,
 | 
				
			||||||
            tension: 0.3,
 | 
					        tension: 0.3,
 | 
				
			||||||
          },
 | 
					      },
 | 
				
			||||||
          {
 | 
					      {
 | 
				
			||||||
            data: data.map((i) => ({x: i.week, y: -i.deletions})),
 | 
					        data: data.map((i) => ({x: i.week, y: -i.deletions})),
 | 
				
			||||||
            pointRadius: 0,
 | 
					        pointRadius: 0,
 | 
				
			||||||
            pointHitRadius: 0,
 | 
					        pointHitRadius: 0,
 | 
				
			||||||
            fill: true,
 | 
					        fill: true,
 | 
				
			||||||
            label: 'Deletions',
 | 
					        label: 'Deletions',
 | 
				
			||||||
            backgroundColor: chartJsColors['deletions'],
 | 
					        backgroundColor: chartJsColors['deletions'],
 | 
				
			||||||
            borderWidth: 0,
 | 
					        borderWidth: 0,
 | 
				
			||||||
            tension: 0.3,
 | 
					        tension: 0.3,
 | 
				
			||||||
          },
 | 
					      },
 | 
				
			||||||
        ],
 | 
					    ],
 | 
				
			||||||
      };
 | 
					  };
 | 
				
			||||||
    },
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getOptions() {
 | 
					const options = {
 | 
				
			||||||
      return {
 | 
					  responsive: true,
 | 
				
			||||||
        responsive: true,
 | 
					  maintainAspectRatio: false,
 | 
				
			||||||
        maintainAspectRatio: false,
 | 
					  animation: true,
 | 
				
			||||||
        animation: true,
 | 
					  plugins: {
 | 
				
			||||||
        plugins: {
 | 
					    legend: {
 | 
				
			||||||
          legend: {
 | 
					      display: true,
 | 
				
			||||||
            display: true,
 | 
					    },
 | 
				
			||||||
          },
 | 
					  },
 | 
				
			||||||
        },
 | 
					  scales: {
 | 
				
			||||||
        scales: {
 | 
					    x: {
 | 
				
			||||||
          x: {
 | 
					      type: 'time',
 | 
				
			||||||
            type: 'time',
 | 
					      grid: {
 | 
				
			||||||
            grid: {
 | 
					        display: false,
 | 
				
			||||||
              display: false,
 | 
					      },
 | 
				
			||||||
            },
 | 
					      time: {
 | 
				
			||||||
            time: {
 | 
					        minUnit: 'month',
 | 
				
			||||||
              minUnit: 'month',
 | 
					      },
 | 
				
			||||||
            },
 | 
					      ticks: {
 | 
				
			||||||
            ticks: {
 | 
					        maxRotation: 0,
 | 
				
			||||||
              maxRotation: 0,
 | 
					        maxTicksLimit: 12,
 | 
				
			||||||
              maxTicksLimit: 12,
 | 
					      },
 | 
				
			||||||
            },
 | 
					    },
 | 
				
			||||||
          },
 | 
					    y: {
 | 
				
			||||||
          y: {
 | 
					      ticks: {
 | 
				
			||||||
            ticks: {
 | 
					        maxTicksLimit: 6,
 | 
				
			||||||
              maxTicksLimit: 6,
 | 
					      },
 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,95 +33,91 @@ Chart.register(
 | 
				
			|||||||
  Tooltip,
 | 
					  Tooltip,
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					defineProps<{
 | 
				
			||||||
  components: {Bar, SvgIcon},
 | 
					  locale: {
 | 
				
			||||||
  props: {
 | 
					    loadingTitle: string;
 | 
				
			||||||
    locale: {
 | 
					    loadingTitleFailed: string;
 | 
				
			||||||
      type: Object,
 | 
					    loadingInfo: string;
 | 
				
			||||||
      required: true,
 | 
					  };
 | 
				
			||||||
    },
 | 
					}>();
 | 
				
			||||||
  },
 | 
					
 | 
				
			||||||
  data: () => ({
 | 
					const isLoading = ref(false);
 | 
				
			||||||
    isLoading: false,
 | 
					const errorText = ref('');
 | 
				
			||||||
    errorText: '',
 | 
					const repoLink = ref(pageData.repoLink || []);
 | 
				
			||||||
    repoLink: pageData.repoLink || [],
 | 
					const data = ref<DayData[]>([]);
 | 
				
			||||||
    data: [],
 | 
					
 | 
				
			||||||
  }),
 | 
					onMounted(() => {
 | 
				
			||||||
  mounted() {
 | 
					  fetchGraphData();
 | 
				
			||||||
    this.fetchGraphData();
 | 
					});
 | 
				
			||||||
  },
 | 
					
 | 
				
			||||||
  methods: {
 | 
					async function fetchGraphData() {
 | 
				
			||||||
    async fetchGraphData() {
 | 
					  isLoading.value = true;
 | 
				
			||||||
      this.isLoading = true;
 | 
					  try {
 | 
				
			||||||
      try {
 | 
					    let response: Response;
 | 
				
			||||||
        let response;
 | 
					    do {
 | 
				
			||||||
        do {
 | 
					      response = await GET(`${repoLink.value}/activity/recent-commits/data`);
 | 
				
			||||||
          response = await GET(`${this.repoLink}/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
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        } 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;
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    } 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) {
 | 
					function toGraphData(data) {
 | 
				
			||||||
      return {
 | 
					  return {
 | 
				
			||||||
        datasets: [
 | 
					    datasets: [
 | 
				
			||||||
          {
 | 
					      {
 | 
				
			||||||
            data: data.map((i) => ({x: i.week, y: i.commits})),
 | 
					        data: data.map((i) => ({x: i.week, y: i.commits})),
 | 
				
			||||||
            label: 'Commits',
 | 
					        label: 'Commits',
 | 
				
			||||||
            backgroundColor: chartJsColors['commits'],
 | 
					        backgroundColor: chartJsColors['commits'],
 | 
				
			||||||
            borderWidth: 0,
 | 
					        borderWidth: 0,
 | 
				
			||||||
            tension: 0.3,
 | 
					        tension: 0.3,
 | 
				
			||||||
          },
 | 
					      },
 | 
				
			||||||
        ],
 | 
					    ],
 | 
				
			||||||
      };
 | 
					  };
 | 
				
			||||||
    },
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getOptions() {
 | 
					const options = {
 | 
				
			||||||
      return {
 | 
					  responsive: true,
 | 
				
			||||||
        responsive: true,
 | 
					  maintainAspectRatio: false,
 | 
				
			||||||
        maintainAspectRatio: false,
 | 
					  animation: true,
 | 
				
			||||||
        animation: true,
 | 
					  scales: {
 | 
				
			||||||
        scales: {
 | 
					    x: {
 | 
				
			||||||
          x: {
 | 
					      type: 'time',
 | 
				
			||||||
            type: 'time',
 | 
					      grid: {
 | 
				
			||||||
            grid: {
 | 
					        display: false,
 | 
				
			||||||
              display: false,
 | 
					      },
 | 
				
			||||||
            },
 | 
					      time: {
 | 
				
			||||||
            time: {
 | 
					        minUnit: 'week',
 | 
				
			||||||
              minUnit: 'week',
 | 
					      },
 | 
				
			||||||
            },
 | 
					      ticks: {
 | 
				
			||||||
            ticks: {
 | 
					        maxRotation: 0,
 | 
				
			||||||
              maxRotation: 0,
 | 
					        maxTicksLimit: 52,
 | 
				
			||||||
              maxTicksLimit: 52,
 | 
					      },
 | 
				
			||||||
            },
 | 
					    },
 | 
				
			||||||
          },
 | 
					    y: {
 | 
				
			||||||
          y: {
 | 
					      ticks: {
 | 
				
			||||||
            ticks: {
 | 
					        maxTicksLimit: 6,
 | 
				
			||||||
              maxTicksLimit: 6,
 | 
					      },
 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					} 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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,78 +1,60 @@
 | 
				
			|||||||
<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 (props.isAdmin) {
 | 
				
			||||||
      if (this.isAdmin) {
 | 
					    categories.push('admin');
 | 
				
			||||||
        categories.push('admin');
 | 
					  }
 | 
				
			||||||
      }
 | 
					  categories.push(
 | 
				
			||||||
      categories.push(
 | 
					    'issue',
 | 
				
			||||||
        'issue',
 | 
					    'misc',
 | 
				
			||||||
        'misc',
 | 
					    'notification',
 | 
				
			||||||
        'notification',
 | 
					    'organization',
 | 
				
			||||||
        'organization',
 | 
					    'package',
 | 
				
			||||||
        'package',
 | 
					    '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">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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');
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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) {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user