mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-09 10:22:32 +00:00
refactor: prepare vue components for vapor mode (#38798)
Removes what would block a later switch to Vue's vapor mode: `vue-chartjs` and the `SvgIcon` render function are virtual DOM components, and `v-memo` has no vapor equivalent. This does not adopt vapor mode, which will be stable in upcoming Vue 3.6. `vue-chartjs` was a thin wrapper over chart.js, so a local `ChartCanvas.vue` replaces it. Chart data and options move into computed values to keep their object identity, which is what `v-memo` was compensating for. `chartjs-adapter-dayjs-4` is moved first-party, just ~40 lines that are easy to maintain.
This commit is contained in:
+1
-3
@@ -37,7 +37,6 @@
|
|||||||
"@vitejs/plugin-vue": "6.0.8",
|
"@vitejs/plugin-vue": "6.0.8",
|
||||||
"asciinema-player": "3.17.0",
|
"asciinema-player": "3.17.0",
|
||||||
"chart.js": "4.5.1",
|
"chart.js": "4.5.1",
|
||||||
"chartjs-adapter-dayjs-4": "1.0.4",
|
|
||||||
"chartjs-plugin-zoom": "2.2.0",
|
"chartjs-plugin-zoom": "2.2.0",
|
||||||
"clippie": "4.2.1",
|
"clippie": "4.2.1",
|
||||||
"codemirror-lang-elixir": "4.0.1",
|
"codemirror-lang-elixir": "4.0.1",
|
||||||
@@ -66,8 +65,7 @@
|
|||||||
"vanilla-colorful": "0.7.2",
|
"vanilla-colorful": "0.7.2",
|
||||||
"vite": "8.1.5",
|
"vite": "8.1.5",
|
||||||
"vite-string-plugin": "2.0.5",
|
"vite-string-plugin": "2.0.5",
|
||||||
"vue": "3.5.40",
|
"vue": "3.5.40"
|
||||||
"vue-chartjs": "5.3.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint-community/eslint-plugin-eslint-comments": "4.7.2",
|
"@eslint-community/eslint-plugin-eslint-comments": "4.7.2",
|
||||||
|
|||||||
Generated
-29
@@ -101,9 +101,6 @@ importers:
|
|||||||
chart.js:
|
chart.js:
|
||||||
specifier: 4.5.1
|
specifier: 4.5.1
|
||||||
version: 4.5.1
|
version: 4.5.1
|
||||||
chartjs-adapter-dayjs-4:
|
|
||||||
specifier: 1.0.4
|
|
||||||
version: 1.0.4(chart.js@4.5.1)(dayjs@1.11.21)
|
|
||||||
chartjs-plugin-zoom:
|
chartjs-plugin-zoom:
|
||||||
specifier: 2.2.0
|
specifier: 2.2.0
|
||||||
version: 2.2.0(chart.js@4.5.1)
|
version: 2.2.0(chart.js@4.5.1)
|
||||||
@@ -191,9 +188,6 @@ importers:
|
|||||||
vue:
|
vue:
|
||||||
specifier: 3.5.40
|
specifier: 3.5.40
|
||||||
version: 3.5.40(typescript@6.0.3)
|
version: 3.5.40(typescript@6.0.3)
|
||||||
vue-chartjs:
|
|
||||||
specifier: 5.3.4
|
|
||||||
version: 5.3.4(chart.js@4.5.1)(vue@3.5.40(typescript@6.0.3))
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint-community/eslint-plugin-eslint-comments':
|
'@eslint-community/eslint-plugin-eslint-comments':
|
||||||
specifier: 4.7.2
|
specifier: 4.7.2
|
||||||
@@ -1834,13 +1828,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==}
|
resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==}
|
||||||
engines: {pnpm: '>=8'}
|
engines: {pnpm: '>=8'}
|
||||||
|
|
||||||
chartjs-adapter-dayjs-4@1.0.4:
|
|
||||||
resolution: {integrity: sha512-yy9BAYW4aNzPVrCWZetbILegTRb7HokhgospPoC3b5iZ5qdlqNmXts2KdSp6AqnjkPAp/YWyHDxLvIvwt5x81w==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
peerDependencies:
|
|
||||||
chart.js: '>=4.0.1'
|
|
||||||
dayjs: ^1.9.7
|
|
||||||
|
|
||||||
chartjs-plugin-zoom@2.2.0:
|
chartjs-plugin-zoom@2.2.0:
|
||||||
resolution: {integrity: sha512-in6kcdiTlP6npIVLMd4zXZ08PDUXC52gZ4FAy5oyjk1zX3gKarXMAof7B9eFiisf9WOC3bh2saHg+J5WtLXZeA==}
|
resolution: {integrity: sha512-in6kcdiTlP6npIVLMd4zXZ08PDUXC52gZ4FAy5oyjk1zX3gKarXMAof7B9eFiisf9WOC3bh2saHg+J5WtLXZeA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4303,12 +4290,6 @@ packages:
|
|||||||
vscode-uri@3.1.0:
|
vscode-uri@3.1.0:
|
||||||
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
||||||
|
|
||||||
vue-chartjs@5.3.4:
|
|
||||||
resolution: {integrity: sha512-x3Fqob8RQvrTdssfi9ecsCzEkFOd8JPmNwSkSQzdfKj/uBsRJs/Y88cZcZIEcPsTVfMGwMo4MOoihoDG2DoE/g==}
|
|
||||||
peerDependencies:
|
|
||||||
chart.js: ^4.1.1
|
|
||||||
vue: ^3.0.0-0 || ^2.7.0
|
|
||||||
|
|
||||||
vue-eslint-parser@10.4.0:
|
vue-eslint-parser@10.4.0:
|
||||||
resolution: {integrity: sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==}
|
resolution: {integrity: sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -6125,11 +6106,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@kurkle/color': 0.3.4
|
'@kurkle/color': 0.3.4
|
||||||
|
|
||||||
chartjs-adapter-dayjs-4@1.0.4(chart.js@4.5.1)(dayjs@1.11.21):
|
|
||||||
dependencies:
|
|
||||||
chart.js: 4.5.1
|
|
||||||
dayjs: 1.11.21
|
|
||||||
|
|
||||||
chartjs-plugin-zoom@2.2.0(chart.js@4.5.1):
|
chartjs-plugin-zoom@2.2.0(chart.js@4.5.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/hammerjs': 2.0.46
|
'@types/hammerjs': 2.0.46
|
||||||
@@ -8999,11 +8975,6 @@ snapshots:
|
|||||||
|
|
||||||
vscode-uri@3.1.0: {}
|
vscode-uri@3.1.0: {}
|
||||||
|
|
||||||
vue-chartjs@5.3.4(chart.js@4.5.1)(vue@3.5.40(typescript@6.0.3)):
|
|
||||||
dependencies:
|
|
||||||
chart.js: 4.5.1
|
|
||||||
vue: 3.5.40(typescript@6.0.3)
|
|
||||||
|
|
||||||
vue-eslint-parser@10.4.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2):
|
vue-eslint-parser@10.4.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@10.2.2)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, nextTick, onBeforeUnmount, onMounted, ref, toRefs, watch} from 'vue';
|
import {computed, nextTick, onBeforeUnmount, onMounted, ref, toRefs, watch} from 'vue';
|
||||||
import {SvgIcon} from '../svg.ts';
|
import SvgIcon from './SvgIcon.vue';
|
||||||
import ActionStatusIcon from './ActionStatusIcon.vue';
|
import ActionStatusIcon from './ActionStatusIcon.vue';
|
||||||
import {addDelegatedEventListener, createElementFromAttrs} from '../utils/dom.ts';
|
import {addDelegatedEventListener, createElementFromAttrs} from '../utils/dom.ts';
|
||||||
import {formatDatetime, formatDatetimeISO} from '../utils/time.ts';
|
import {formatDatetime, formatDatetimeISO} from '../utils/time.ts';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {computed} from 'vue';
|
import {computed} from 'vue';
|
||||||
import {SvgIcon} from '../svg.ts';
|
import SvgIcon from './SvgIcon.vue';
|
||||||
import {getActionStatusIcon, type ActionStatusIconVariant} from '../modules/action-status-icon.ts';
|
import {getActionStatusIcon, type ActionStatusIconVariant} from '../modules/action-status-icon.ts';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import {onMounted, onUnmounted, toRaw, useTemplateRef, watch, type ShallowRef} from 'vue';
|
||||||
|
import {_adapters, BarController, Chart, LinearScale, LineController, TimeScale, type ChartData, type ChartOptions, type TimeUnit} from 'chart.js';
|
||||||
|
import {chartJsColors} from '../utils/color.ts';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import advancedFormat from 'dayjs/plugin/advancedFormat.js';
|
||||||
|
import quarterOfYear from 'dayjs/plugin/quarterOfYear.js';
|
||||||
|
import type {ConfigType, ManipulateType} from 'dayjs';
|
||||||
|
|
||||||
|
dayjs.extend(advancedFormat); // the quarter format needs the `Q` token
|
||||||
|
dayjs.extend(quarterOfYear);
|
||||||
|
|
||||||
|
Chart.defaults.color = chartJsColors.text;
|
||||||
|
Chart.defaults.borderColor = chartJsColors.border;
|
||||||
|
Chart.register(BarController, LineController, LinearScale, TimeScale);
|
||||||
|
|
||||||
|
// minimal port of chartjs-adapter-dayjs-4, MIT license, Copyright (c) 2022 bolstycjw
|
||||||
|
_adapters._date.override({
|
||||||
|
formats: () => ({
|
||||||
|
datetime: 'MMM D, YYYY, h:mm:ss a',
|
||||||
|
millisecond: 'h:mm:ss.SSS a',
|
||||||
|
second: 'h:mm:ss a',
|
||||||
|
minute: 'h:mm a',
|
||||||
|
hour: 'hA',
|
||||||
|
day: 'MMM D',
|
||||||
|
week: 'MMM D, YYYY',
|
||||||
|
month: 'MMM YYYY',
|
||||||
|
quarter: '[Q]Q - YYYY',
|
||||||
|
year: 'YYYY',
|
||||||
|
}),
|
||||||
|
parse: (value: ConfigType) => {
|
||||||
|
const date = dayjs(value);
|
||||||
|
return date.isValid() ? date.valueOf() : null;
|
||||||
|
},
|
||||||
|
format: (time: number, format: string) => dayjs(time).format(format),
|
||||||
|
add: (time: number, amount: number, unit: TimeUnit) => dayjs(time).add(amount, unit as ManipulateType).valueOf(), // the quarter plugin widens this at runtime
|
||||||
|
diff: (max: number, min: number, unit: TimeUnit) => dayjs(max).diff(min, unit),
|
||||||
|
// chart.js only asks for `isoWeek` when `time.isoWeekday` is set, which we never do
|
||||||
|
startOf: (time: number, unit: TimeUnit) => dayjs(time).startOf(unit).valueOf(),
|
||||||
|
endOf: (time: number, unit: TimeUnit) => dayjs(time).endOf(unit).valueOf(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
type: 'bar' | 'line',
|
||||||
|
data: ChartData,
|
||||||
|
options: ChartOptions,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const elCanvas = useTemplateRef('elCanvas') as Readonly<ShallowRef<HTMLCanvasElement>>;
|
||||||
|
let chart: Chart | undefined;
|
||||||
|
|
||||||
|
// chart.js mutates what it gets, so it must never see a reactive proxy
|
||||||
|
onMounted(() => {
|
||||||
|
chart = new Chart(elCanvas.value, {type: props.type, data: toRaw(props.data), options: toRaw(props.options)});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
chart?.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([() => props.data, () => props.options], ([data, options]) => {
|
||||||
|
if (!chart) return; // chart creation failed
|
||||||
|
chart.data = toRaw(data);
|
||||||
|
chart.options = toRaw(options);
|
||||||
|
chart.update();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<canvas ref="elCanvas" role="img"/>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {SvgIcon} from '../svg.ts';
|
import SvgIcon from './SvgIcon.vue';
|
||||||
import {getIssueColorClass, getIssueIcon} from '../features/issue.ts';
|
import {getIssueColorClass, getIssueIcon} from '../features/issue.ts';
|
||||||
import {computed} from 'vue';
|
import {computed} from 'vue';
|
||||||
import type {Issue} from '../types.ts';
|
import type {Issue} from '../types.ts';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {computed, nextTick, onMounted, shallowRef, useTemplateRef, type ShallowRef} from 'vue';
|
import {computed, nextTick, onMounted, shallowRef, useTemplateRef, type ShallowRef} from 'vue';
|
||||||
import {SvgIcon} from '../svg.ts';
|
import SvgIcon from './SvgIcon.vue';
|
||||||
import {GET} from '../modules/fetch.ts';
|
import {GET} from '../modules/fetch.ts';
|
||||||
import {urlQueryEscape} from '../utils/url.ts';
|
import {urlQueryEscape} from '../utils/url.ts';
|
||||||
import type {SvgName} from '../svg.ts';
|
import type {SvgName} from '../svg.ts';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, useTemplateRef, type ShallowRef} from 'vue';
|
import {computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, useTemplateRef, type ShallowRef} from 'vue';
|
||||||
import {SvgIcon} from '../svg.ts';
|
import SvgIcon from './SvgIcon.vue';
|
||||||
import {GET} from '../modules/fetch.ts';
|
import {GET} from '../modules/fetch.ts';
|
||||||
import {generateElemId} from '../utils/dom.ts';
|
import {generateElemId} from '../utils/dom.ts';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {SvgIcon, type SvgName} from '../svg.ts';
|
import SvgIcon from './SvgIcon.vue';
|
||||||
|
import type {SvgName} from '../svg.ts';
|
||||||
import {shallowRef} from 'vue';
|
import {shallowRef} from 'vue';
|
||||||
import {type DiffStatus, type DiffTreeEntry, diffTreeStore} from '../modules/diff-file.ts';
|
import {type DiffStatus, type DiffTreeEntry, diffTreeStore} from '../modules/diff-file.ts';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {computed, onMounted, onUnmounted, shallowRef, watch} from 'vue';
|
import {computed, onMounted, onUnmounted, shallowRef, watch} from 'vue';
|
||||||
import {SvgIcon} from '../svg.ts';
|
import SvgIcon from './SvgIcon.vue';
|
||||||
import {toggleElem} from '../utils/dom.ts';
|
import {toggleElem} from '../utils/dom.ts';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {SvgIcon} from '../svg.ts';
|
import SvgIcon from './SvgIcon.vue';
|
||||||
import ActionStatusIcon from './ActionStatusIcon.vue';
|
import ActionStatusIcon from './ActionStatusIcon.vue';
|
||||||
import {computed, onBeforeUnmount, ref, toRefs, watch} from 'vue';
|
import {computed, onBeforeUnmount, ref, toRefs, watch} from 'vue';
|
||||||
import {resetActionFavicon, syncActionRunFavicon} from '../modules/favicon-status.ts';
|
import {resetActionFavicon, syncActionRunFavicon} from '../modules/favicon-status.ts';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {computed, nextTick, onBeforeUnmount, onMounted, shallowRef, useTemplateRef, watch, type ShallowRef} from 'vue';
|
import {computed, nextTick, onBeforeUnmount, onMounted, shallowRef, useTemplateRef, watch, type ShallowRef} from 'vue';
|
||||||
import {SvgIcon} from '../svg.ts';
|
import SvgIcon from './SvgIcon.vue';
|
||||||
import {showErrorToast} from '../modules/toast.ts';
|
import {showErrorToast} from '../modules/toast.ts';
|
||||||
import {GET} from '../modules/fetch.ts';
|
import {GET} from '../modules/fetch.ts';
|
||||||
import {pathEscapeSegments} from '../utils/url.ts';
|
import {pathEscapeSegments} from '../utils/url.ts';
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {SvgIcon} from '../svg.ts';
|
import SvgIcon from './SvgIcon.vue';
|
||||||
import {
|
import {
|
||||||
Chart,
|
Chart,
|
||||||
Legend,
|
Legend,
|
||||||
LinearScale,
|
|
||||||
TimeScale,
|
|
||||||
PointElement,
|
PointElement,
|
||||||
LineElement,
|
LineElement,
|
||||||
Filler,
|
Filler,
|
||||||
@@ -12,7 +10,7 @@ import {
|
|||||||
type ChartData,
|
type ChartData,
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import {GET} from '../modules/fetch.ts';
|
import {GET} from '../modules/fetch.ts';
|
||||||
import {Line as ChartLine} from 'vue-chartjs';
|
import ChartCanvas from './ChartCanvas.vue';
|
||||||
import {
|
import {
|
||||||
startDaysBetween,
|
startDaysBetween,
|
||||||
firstStartDateAfterDate,
|
firstStartDateAfterDate,
|
||||||
@@ -23,17 +21,11 @@ import {
|
|||||||
import {chartJsColors} from '../utils/color.ts';
|
import {chartJsColors} from '../utils/color.ts';
|
||||||
import {errorMessage} from '../modules/errors.ts';
|
import {errorMessage} from '../modules/errors.ts';
|
||||||
import {sleep} from '../utils.ts';
|
import {sleep} from '../utils.ts';
|
||||||
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
|
import {computed, onMounted, shallowRef} from 'vue';
|
||||||
import {onMounted, shallowRef} from 'vue';
|
|
||||||
|
|
||||||
const {pageData} = window.config;
|
const {pageData} = window.config;
|
||||||
|
|
||||||
Chart.defaults.color = chartJsColors.text;
|
|
||||||
Chart.defaults.borderColor = chartJsColors.border;
|
|
||||||
|
|
||||||
Chart.register(
|
Chart.register(
|
||||||
TimeScale,
|
|
||||||
LinearScale,
|
|
||||||
Legend,
|
Legend,
|
||||||
PointElement,
|
PointElement,
|
||||||
LineElement,
|
LineElement,
|
||||||
@@ -85,7 +77,9 @@ async function fetchGraphData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toGraphData(data: Array<Record<string, any>>): ChartData<'line'> {
|
const graphData = computed(() => toGraphData(data.value));
|
||||||
|
|
||||||
|
function toGraphData(data: DayData[]): ChartData<'line'> {
|
||||||
return {
|
return {
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
@@ -159,9 +153,9 @@ const options: ChartOptions<'line'> = {
|
|||||||
{{ errorText }}
|
{{ errorText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChartLine
|
<ChartCanvas
|
||||||
v-memo="data" v-if="data.length !== 0"
|
v-if="data.length !== 0"
|
||||||
:data="toGraphData(data)" :options="options"
|
type="line" :data="graphData" :options="options"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {computed, onMounted, shallowRef} from 'vue';
|
import {computed, onMounted, shallowRef} from 'vue';
|
||||||
import {SvgIcon} from '../svg.ts';
|
import SvgIcon from './SvgIcon.vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import {GET} from '../modules/fetch.ts';
|
import {GET} from '../modules/fetch.ts';
|
||||||
import {Line as ChartLine} from 'vue-chartjs';
|
import ChartCanvas from './ChartCanvas.vue';
|
||||||
import {
|
import {
|
||||||
Chart,
|
Chart,
|
||||||
Title,
|
Title,
|
||||||
BarElement,
|
|
||||||
LinearScale,
|
|
||||||
TimeScale,
|
|
||||||
PointElement,
|
PointElement,
|
||||||
LineElement,
|
LineElement,
|
||||||
Filler,
|
Filler,
|
||||||
@@ -19,7 +16,6 @@ import {
|
|||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import zoomPlugin from 'chartjs-plugin-zoom';
|
import zoomPlugin from 'chartjs-plugin-zoom';
|
||||||
import {chartJsColors} from '../utils/color.ts';
|
import {chartJsColors} from '../utils/color.ts';
|
||||||
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
|
|
||||||
import {
|
import {
|
||||||
startDaysBetween,
|
startDaysBetween,
|
||||||
firstStartDateAfterDate,
|
firstStartDateAfterDate,
|
||||||
@@ -56,13 +52,7 @@ type LineOptions = ChartOptions<'line'> & {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Chart.defaults.color = chartJsColors.text;
|
|
||||||
Chart.defaults.borderColor = chartJsColors.border;
|
|
||||||
|
|
||||||
Chart.register(
|
Chart.register(
|
||||||
TimeScale,
|
|
||||||
LinearScale,
|
|
||||||
BarElement,
|
|
||||||
Title,
|
Title,
|
||||||
PointElement,
|
PointElement,
|
||||||
LineElement,
|
LineElement,
|
||||||
@@ -102,7 +92,8 @@ const errorText = shallowRef('');
|
|||||||
const totalStats = shallowRef<Record<string, any>>({});
|
const totalStats = shallowRef<Record<string, any>>({});
|
||||||
const sortedContributors = shallowRef<Array<Record<string, any>>>([]);
|
const sortedContributors = shallowRef<Array<Record<string, any>>>([]);
|
||||||
const type = shallowRef<ContributionType>('commits');
|
const type = shallowRef<ContributionType>('commits');
|
||||||
let contributorsStats: Record<string, any> = {}; // these three are not read during render
|
let contributorsStats: Record<string, any> = {};
|
||||||
|
// plain values, so the main chart options do not follow the zoomed range
|
||||||
let xAxisStart: number | null = null;
|
let xAxisStart: number | null = null;
|
||||||
let xAxisEnd: number | null = null;
|
let xAxisEnd: number | null = null;
|
||||||
const xAxisMin = shallowRef<number | null>(null);
|
const xAxisMin = shallowRef<number | null>(null);
|
||||||
@@ -301,8 +292,9 @@ function getOptions(chartType: ChartType): LineOptions {
|
|||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
min: xAxisMin.value ?? undefined,
|
// the main chart keeps its own zoom range
|
||||||
max: xAxisMax.value ?? undefined,
|
min: (chartType === 'main' ? xAxisStart : xAxisMin.value) ?? undefined,
|
||||||
|
max: (chartType === 'main' ? xAxisEnd : xAxisMax.value) ?? undefined,
|
||||||
type: 'time',
|
type: 'time',
|
||||||
grid: {
|
grid: {
|
||||||
display: false,
|
display: false,
|
||||||
@@ -325,6 +317,17 @@ function getOptions(chartType: ChartType): LineOptions {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mainChart = computed(() => ({
|
||||||
|
graphData: toGraphData(totalStats.value.weeks),
|
||||||
|
chartOptions: getOptions('main'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const contributorCharts = computed(() => sortedContributors.value.map((contributor) => ({
|
||||||
|
contributor,
|
||||||
|
graphData: toGraphData(contributor.weeks),
|
||||||
|
chartOptions: getOptions('contributor'), // chart.js mutates it, so each chart needs its own
|
||||||
|
})));
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
@@ -386,16 +389,15 @@ function getOptions(chartType: ChartType): LineOptions {
|
|||||||
{{ errorText }}
|
{{ errorText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChartLine
|
<ChartCanvas
|
||||||
v-memo="[totalStats.weeks, type]" v-if="Object.keys(totalStats).length !== 0"
|
v-if="Object.keys(totalStats).length !== 0"
|
||||||
:data="toGraphData(totalStats.weeks)" :options="getOptions('main')"
|
type="line" :data="mainChart.graphData" :options="mainChart.chartOptions"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="contributor-grid">
|
<div class="contributor-grid">
|
||||||
<div
|
<div
|
||||||
v-for="(contributor, index) in sortedContributors"
|
v-for="({contributor, graphData, chartOptions}, index) in contributorCharts"
|
||||||
:key="index"
|
:key="index"
|
||||||
v-memo="[sortedContributors, type]"
|
|
||||||
>
|
>
|
||||||
<div class="ui top attached header tw-flex tw-flex-1">
|
<div class="ui top attached header tw-flex tw-flex-1">
|
||||||
<b class="ui right">#{{ index + 1 }}</b>
|
<b class="ui right">#{{ index + 1 }}</b>
|
||||||
@@ -421,9 +423,10 @@ function getOptions(chartType: ChartType): LineOptions {
|
|||||||
</div>
|
</div>
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
<div>
|
<div>
|
||||||
<ChartLine
|
<ChartCanvas
|
||||||
:data="toGraphData(contributor.weeks)"
|
type="line"
|
||||||
:options="getOptions('contributor')"
|
:data="graphData"
|
||||||
|
:options="chartOptions"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {generateElemId} from '../utils/dom.ts';
|
|||||||
import {GET} from '../modules/fetch.ts';
|
import {GET} from '../modules/fetch.ts';
|
||||||
import {filterRepoFilesWeighted} from '../features/repo-findfile.ts';
|
import {filterRepoFilesWeighted} from '../features/repo-findfile.ts';
|
||||||
import {pathEscapeSegments} from '../utils/url.ts';
|
import {pathEscapeSegments} from '../utils/url.ts';
|
||||||
import {SvgIcon} from '../svg.ts';
|
import SvgIcon from './SvgIcon.vue';
|
||||||
import {throttle} from '../utils/func.ts';
|
import {throttle} from '../utils/func.ts';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {SvgIcon} from '../svg.ts';
|
import SvgIcon from './SvgIcon.vue';
|
||||||
import {
|
import {
|
||||||
Chart,
|
Chart,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
BarElement,
|
BarElement,
|
||||||
LinearScale,
|
|
||||||
TimeScale,
|
|
||||||
type ChartOptions,
|
type ChartOptions,
|
||||||
type ChartData,
|
type ChartData,
|
||||||
type ChartDataset,
|
type ChartDataset,
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import {GET} from '../modules/fetch.ts';
|
import {GET} from '../modules/fetch.ts';
|
||||||
import {Bar} from 'vue-chartjs';
|
import ChartCanvas from './ChartCanvas.vue';
|
||||||
import {
|
import {
|
||||||
startDaysBetween,
|
startDaysBetween,
|
||||||
firstStartDateAfterDate,
|
firstStartDateAfterDate,
|
||||||
@@ -22,17 +20,11 @@ import {
|
|||||||
import {chartJsColors} from '../utils/color.ts';
|
import {chartJsColors} from '../utils/color.ts';
|
||||||
import {errorMessage} from '../modules/errors.ts';
|
import {errorMessage} from '../modules/errors.ts';
|
||||||
import {sleep} from '../utils.ts';
|
import {sleep} from '../utils.ts';
|
||||||
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
|
import {computed, onMounted, shallowRef} from 'vue';
|
||||||
import {onMounted, ref, shallowRef} from 'vue';
|
|
||||||
|
|
||||||
const {pageData} = window.config;
|
const {pageData} = window.config;
|
||||||
|
|
||||||
Chart.defaults.color = chartJsColors.text;
|
|
||||||
Chart.defaults.borderColor = chartJsColors.border;
|
|
||||||
|
|
||||||
Chart.register(
|
Chart.register(
|
||||||
TimeScale,
|
|
||||||
LinearScale,
|
|
||||||
BarElement,
|
BarElement,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
);
|
);
|
||||||
@@ -48,7 +40,7 @@ defineProps<{
|
|||||||
const isLoading = shallowRef(false);
|
const isLoading = shallowRef(false);
|
||||||
const errorText = shallowRef('');
|
const errorText = shallowRef('');
|
||||||
const repoLink = pageData.repoLink!;
|
const repoLink = pageData.repoLink!;
|
||||||
const data = ref<DayData[]>([]);
|
const data = shallowRef<DayData[]>([]);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchGraphData();
|
fetchGraphData();
|
||||||
@@ -81,6 +73,8 @@ async function fetchGraphData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const graphData = computed(() => toGraphData(data.value));
|
||||||
|
|
||||||
function toGraphData(data: DayData[]): ChartData<'bar'> {
|
function toGraphData(data: DayData[]): ChartData<'bar'> {
|
||||||
return {
|
return {
|
||||||
datasets: [
|
datasets: [
|
||||||
@@ -137,9 +131,9 @@ const options: ChartOptions<'bar'> = {
|
|||||||
{{ errorText }}
|
{{ errorText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Bar
|
<ChartCanvas
|
||||||
v-memo="data" v-if="data.length !== 0"
|
v-if="data.length !== 0"
|
||||||
:data="toGraphData(data)" :options="options"
|
type="bar" :data="graphData" :options="options"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import SvgIcon from './SvgIcon.vue';
|
||||||
|
import {createApp, h} from 'vue';
|
||||||
|
|
||||||
|
test('SvgIcon', () => {
|
||||||
|
const root = document.createElement('div');
|
||||||
|
createApp({render: () => h(SvgIcon, {name: 'octicon-dot-fill', size: 24, class: 'base', symbolId: 'svg-symbol-dot'})}).mount(root);
|
||||||
|
expect(root.innerHTML).toBe(
|
||||||
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24" aria-hidden="true" class="svg octicon-dot-fill tw-hidden svg-symbol-container base"><symbol id="svg-symbol-dot" viewBox="0 0 16 16"><path d="M8 4a4 4 0 1 1 0 8 4 4 0 0 1 0-8"></path></symbol></svg>`,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import {computed} from 'vue';
|
||||||
|
import {svgParseOuterInner, type SvgName} from '../svg.ts';
|
||||||
|
import {html, htmlRaw} from '../utils/html.ts';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
name: SvgName,
|
||||||
|
size?: number,
|
||||||
|
symbolId?: string,
|
||||||
|
}>(), {
|
||||||
|
size: 16,
|
||||||
|
symbolId: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const icon = computed(() => {
|
||||||
|
let {svgOuter, svgInnerHtml} = svgParseOuterInner(props.name);
|
||||||
|
const attrs: Record<string, string | number> = {};
|
||||||
|
for (const attr of svgOuter.attributes) {
|
||||||
|
if (attr.name === 'class') continue;
|
||||||
|
attrs[attr.name] = attr.value;
|
||||||
|
}
|
||||||
|
attrs.width = props.size;
|
||||||
|
attrs.height = props.size;
|
||||||
|
|
||||||
|
const classes = Array.from(svgOuter.classList);
|
||||||
|
if (props.symbolId) {
|
||||||
|
classes.push('tw-hidden', 'svg-symbol-container');
|
||||||
|
svgInnerHtml = html`<symbol id="${props.symbolId}" viewBox="${attrs.viewBox}">${htmlRaw(svgInnerHtml)}</symbol>`;
|
||||||
|
}
|
||||||
|
attrs.innerHTML = svgInnerHtml; // the icons are bundled, they carry no user input
|
||||||
|
return {attrs, classes};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg v-bind="icon.attrs" :class="icon.classes"/>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {SvgIcon} from '../svg.ts';
|
import SvgIcon from './SvgIcon.vue';
|
||||||
import {isPlainClick} from '../utils/dom.ts';
|
import {isPlainClick} from '../utils/dom.ts';
|
||||||
import {shouldTriggerAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
import {shouldTriggerAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
||||||
import {shallowRef} from 'vue';
|
import {shallowRef} from 'vue';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, onMounted, onUnmounted, ref, watch} from 'vue';
|
import {computed, onMounted, onUnmounted, ref, watch} from 'vue';
|
||||||
import {SvgIcon} from '../svg.ts';
|
import SvgIcon from './SvgIcon.vue';
|
||||||
import ActionStatusIcon from './ActionStatusIcon.vue';
|
import ActionStatusIcon from './ActionStatusIcon.vue';
|
||||||
import {localUserSettings} from '../modules/user-settings.ts';
|
import {localUserSettings} from '../modules/user-settings.ts';
|
||||||
import {isPlainClick} from '../utils/dom.ts';
|
import {isPlainClick} from '../utils/dom.ts';
|
||||||
|
|||||||
+1
-13
@@ -1,5 +1,4 @@
|
|||||||
import {svg, SvgIcon, svgParseOuterInner} from './svg.ts';
|
import {svg, svgParseOuterInner} from './svg.ts';
|
||||||
import {createApp, h} from 'vue';
|
|
||||||
|
|
||||||
test('svg', () => {
|
test('svg', () => {
|
||||||
expect(svg('octicon-repo')).toMatch(/^<svg/);
|
expect(svg('octicon-repo')).toMatch(/^<svg/);
|
||||||
@@ -13,14 +12,3 @@ test('svgParseOuterInner', () => {
|
|||||||
expect(svgOuter.classList.contains('octicon-repo')).toBeTruthy();
|
expect(svgOuter.classList.contains('octicon-repo')).toBeTruthy();
|
||||||
expect(svgInnerHtml).toContain('<path');
|
expect(svgInnerHtml).toContain('<path');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SvgIcon', () => {
|
|
||||||
const root = document.createElement('div');
|
|
||||||
createApp({render: () => h(SvgIcon, {name: 'octicon-link', size: 24, class: 'base'})}).mount(root);
|
|
||||||
const node = root.firstChild as Element;
|
|
||||||
expect(node.nodeName).toEqual('svg');
|
|
||||||
expect(node.getAttribute('width')).toEqual('24');
|
|
||||||
expect(node.getAttribute('height')).toEqual('24');
|
|
||||||
expect(node.classList.contains('octicon-link')).toBeTruthy();
|
|
||||||
expect(node.classList.contains('base')).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|||||||
+1
-35
@@ -1,6 +1,5 @@
|
|||||||
import {defineComponent, h, type PropType} from 'vue';
|
|
||||||
import {parseDom, serializeXml} from './utils.ts';
|
import {parseDom, serializeXml} from './utils.ts';
|
||||||
import {html, htmlRaw} from './utils/html.ts';
|
import {htmlRaw} from './utils/html.ts';
|
||||||
import giteaDoubleChevronLeft from '../../public/assets/img/svg/gitea-double-chevron-left.svg';
|
import giteaDoubleChevronLeft from '../../public/assets/img/svg/gitea-double-chevron-left.svg';
|
||||||
import giteaDoubleChevronRight from '../../public/assets/img/svg/gitea-double-chevron-right.svg';
|
import giteaDoubleChevronRight from '../../public/assets/img/svg/gitea-double-chevron-right.svg';
|
||||||
import giteaEmptyCheckbox from '../../public/assets/img/svg/gitea-empty-checkbox.svg';
|
import giteaEmptyCheckbox from '../../public/assets/img/svg/gitea-empty-checkbox.svg';
|
||||||
@@ -222,36 +221,3 @@ export function svgParseOuterInner(name: SvgName) {
|
|||||||
const svgOuter = svgDoc.firstChild as SVGElement;
|
const svgOuter = svgDoc.firstChild as SVGElement;
|
||||||
return {svgOuter, svgInnerHtml};
|
return {svgOuter, svgInnerHtml};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SvgIcon = defineComponent({
|
|
||||||
name: 'SvgIcon',
|
|
||||||
props: {
|
|
||||||
name: {type: String as PropType<SvgName>, required: true},
|
|
||||||
size: {type: Number, default: 16},
|
|
||||||
symbolId: {type: String},
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
let {svgOuter, svgInnerHtml} = svgParseOuterInner(this.name);
|
|
||||||
// https://vuejs.org/guide/extras/render-function.html#creating-vnodes
|
|
||||||
// the `^` is used for attr, set SVG attributes like 'width', `aria-hidden`, `viewBox`, etc
|
|
||||||
const attrs: Record<string, any> = {};
|
|
||||||
for (const attr of svgOuter.attributes) {
|
|
||||||
if (attr.name === 'class') continue;
|
|
||||||
attrs[`^${attr.name}`] = attr.value;
|
|
||||||
}
|
|
||||||
attrs[`^width`] = this.size;
|
|
||||||
attrs[`^height`] = this.size;
|
|
||||||
|
|
||||||
const classes = Array.from(svgOuter.classList);
|
|
||||||
if (this.symbolId) {
|
|
||||||
classes.push('tw-hidden', 'svg-symbol-container');
|
|
||||||
svgInnerHtml = html`<symbol id="${this.symbolId}" viewBox="${attrs['^viewBox']}">${htmlRaw(svgInnerHtml)}</symbol>`;
|
|
||||||
}
|
|
||||||
// create VNode
|
|
||||||
return h('svg', {
|
|
||||||
...attrs,
|
|
||||||
class: classes,
|
|
||||||
innerHTML: svgInnerHtml,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user