mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 20:36:07 +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.
 | 
			
		||||
    action status accepted: success, skipped, waiting, blocked, running, failure, cancelled, unknown
 | 
			
		||||
-->
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import {SvgIcon} from '../svg.ts';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  components: {SvgIcon},
 | 
			
		||||
  props: {
 | 
			
		||||
    status: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
    size: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      default: 16,
 | 
			
		||||
    },
 | 
			
		||||
    className: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '',
 | 
			
		||||
    },
 | 
			
		||||
    localeStatus: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
withDefaults(defineProps<{
 | 
			
		||||
  status: '',
 | 
			
		||||
  size?: number,
 | 
			
		||||
  className?: string,
 | 
			
		||||
  localeStatus?: string,
 | 
			
		||||
}>(), {
 | 
			
		||||
  size: 16,
 | 
			
		||||
  className: undefined,
 | 
			
		||||
  localeStatus: undefined,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <span class="tw-flex tw-items-center" :data-tooltip-content="localeStatus" v-if="status">
 | 
			
		||||
    <SvgIcon name="octicon-check-circle-fill" class="text green" :size="size" :class-name="className" v-if="status === 'success'"/>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,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">
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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">
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
          };
 | 
			
		||||
const store = diffTreeStore();
 | 
			
		||||
 | 
			
		||||
          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 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;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    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 (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);
 | 
			
		||||
      }
 | 
			
		||||
      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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,84 +1,83 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import {computed, onMounted, onUnmounted, ref, watch} from 'vue';
 | 
			
		||||
import {SvgIcon} from '../svg.ts';
 | 
			
		||||
import {toggleElem} from '../utils/dom.ts';
 | 
			
		||||
 | 
			
		||||
const {csrfToken, pageData} = window.config;
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  components: {SvgIcon},
 | 
			
		||||
  data: () => ({
 | 
			
		||||
    csrfToken,
 | 
			
		||||
    mergeForm: pageData.pullRequestMergeForm,
 | 
			
		||||
const mergeForm = ref(pageData.pullRequestMergeForm);
 | 
			
		||||
 | 
			
		||||
    mergeTitleFieldValue: '',
 | 
			
		||||
    mergeMessageFieldValue: '',
 | 
			
		||||
    deleteBranchAfterMerge: false,
 | 
			
		||||
    autoMergeWhenSucceed: false,
 | 
			
		||||
const mergeTitleFieldValue = ref('');
 | 
			
		||||
const mergeMessageFieldValue = ref('');
 | 
			
		||||
const deleteBranchAfterMerge = ref(false);
 | 
			
		||||
const autoMergeWhenSucceed = ref(false);
 | 
			
		||||
 | 
			
		||||
    mergeStyle: '',
 | 
			
		||||
    mergeStyleDetail: { // dummy only, these values will come from one of the mergeForm.mergeStyles
 | 
			
		||||
      hideMergeMessageTexts: false,
 | 
			
		||||
      textDoMerge: '',
 | 
			
		||||
      mergeTitleFieldText: '',
 | 
			
		||||
      mergeMessageFieldText: '',
 | 
			
		||||
      hideAutoMerge: false,
 | 
			
		||||
    },
 | 
			
		||||
    mergeStyleAllowedCount: 0,
 | 
			
		||||
const mergeStyle = ref('');
 | 
			
		||||
const mergeStyleDetail = ref({
 | 
			
		||||
  hideMergeMessageTexts: false,
 | 
			
		||||
  textDoMerge: '',
 | 
			
		||||
  mergeTitleFieldText: '',
 | 
			
		||||
  mergeMessageFieldText: '',
 | 
			
		||||
  hideAutoMerge: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
const mergeStyleAllowedCount = ref(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 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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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">
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user