diff --git a/report-viewer/src/components/fileDisplaying/FilesContainer.vue b/report-viewer/src/components/fileDisplaying/FilesContainer.vue index 7594bfb53..ede1c03ba 100644 --- a/report-viewer/src/components/fileDisplaying/FilesContainer.vue +++ b/report-viewer/src/components/fileDisplaying/FilesContainer.vue @@ -16,15 +16,13 @@ - + { + const matches: Record = {} + for (const file of props.files) { + matches[file.fileName] = !props.matches.get(file.fileName) + ? [] + : (props.matches.get(file.fileName) as MatchInSingleFile[]) + } + return matches +}) + +const sortedFiles: Ref = ref([]) +sortFiles(store().uiState.fileSorting ?? FileSortingOptions.ALPHABETICAL) + +function sortFiles(fileSorting: FileSortingOptions) { + switch (fileSorting) { + case FileSortingOptions.ALPHABETICAL: { + sortedFiles.value = Array.from(props.files).sort((a, b) => + a.fileName.localeCompare(b.fileName) + ) + break + } + + case FileSortingOptions.MATCH_SIZE: { + const largestMatch: Record = {} + for (const file of props.files) { + largestMatch[file.fileName] = Math.max( + ...matchesPerFile.value[file.fileName].map((match) => match.match.tokens) + ) + } + sortedFiles.value = Array.from(props.files).sort( + (a, b) => largestMatch[b.fileName] - largestMatch[a.fileName] + ) + break + } + + case FileSortingOptions.MATCH_COUNT: { + const matchCount: Record = {} + for (const file of props.files) { + matchCount[file.fileName] = matchesPerFile.value[file.fileName].length + } + sortedFiles.value = Array.from(props.files).sort( + (a, b) => matchCount[b.fileName] - matchCount[a.fileName] + ) + break + } + + case FileSortingOptions.MATCH_COVERAGE: { + const matchCoverage: Record = {} + for (const file of props.files) { + const matches = matchesPerFile.value[file.fileName] + const totalTokens = matches.reduce((acc, match) => acc + match.match.tokens, 0) + matchCoverage[file.fileName] = + totalTokens / (file.tokenCount > 0 ? file.tokenCount : Infinity) + } + sortedFiles.value = Array.from(props.files).sort( + (a, b) => matchCoverage[b.fileName] - matchCoverage[a.fileName] + ) + break + } + } +} + +const shouldEmitFileMoving = ref(true) + +function emitFileMoving() { + if (!shouldEmitFileMoving.value) { + return + } + emit('filesMoved') +} const codePanels: Ref<(typeof CodePanel)[]> = ref([]) const scrollContainer: Ref = ref(null) @@ -132,6 +203,7 @@ function collapseAll() { } defineExpose({ - scrollTo + scrollTo, + sortFiles }) diff --git a/report-viewer/src/components/optionsSelectors/OptionsSelectorComponent.vue b/report-viewer/src/components/optionsSelectors/OptionsSelectorComponent.vue index 6be436228..b8abfd9f4 100644 --- a/report-viewer/src/components/optionsSelectors/OptionsSelectorComponent.vue +++ b/report-viewer/src/components/optionsSelectors/OptionsSelectorComponent.vue @@ -73,4 +73,8 @@ function select(index: number) { emit('selectionChanged', index) selected.value = index } + +defineExpose({ + select +}) diff --git a/report-viewer/src/model/ui/FileSortingOptions.ts b/report-viewer/src/model/ui/FileSortingOptions.ts new file mode 100644 index 000000000..8fe3d4837 --- /dev/null +++ b/report-viewer/src/model/ui/FileSortingOptions.ts @@ -0,0 +1,27 @@ +import type { ToolTipLabel } from './ToolTip' + +export enum FileSortingOptions { + ALPHABETICAL, + MATCH_COVERAGE, + MATCH_COUNT, + MATCH_SIZE +} + +export const fileSortingTooltips: Record = { + [FileSortingOptions.ALPHABETICAL]: { + displayValue: 'Alphabetical', + tooltip: 'Sort files alphabetically, similar to package structure.' + }, + [FileSortingOptions.MATCH_COVERAGE]: { + displayValue: 'Match Coverage', + tooltip: 'Sort files by the percentage of tokens included in a match.' + }, + [FileSortingOptions.MATCH_COUNT]: { + displayValue: 'Match Count', + tooltip: 'Sort files by the number of matches found.' + }, + [FileSortingOptions.MATCH_SIZE]: { + displayValue: 'Match Size', + tooltip: 'Sort files by match size, with the largest matches at the top.' + } +} diff --git a/report-viewer/src/stores/state.ts b/report-viewer/src/stores/state.ts index 8ef4a0521..b1f031c87 100644 --- a/report-viewer/src/stores/state.ts +++ b/report-viewer/src/stores/state.ts @@ -1,6 +1,7 @@ import type { SubmissionFile } from '@/model/File' import type { MetricType } from '@/model/MetricType' import type { DistributionChartConfig } from '@/model/ui/DistributionChartConfig' +import type { FileSortingOptions } from '@/model/ui/FileSortingOptions' /** * Local store. Stores the state of the application. @@ -49,4 +50,5 @@ export interface UIState { comparisonTableSortingMetric: MetricType comparisonTableClusterSorting: boolean distributionChartConfig: DistributionChartConfig + fileSorting: FileSortingOptions } diff --git a/report-viewer/src/stores/store.ts b/report-viewer/src/stores/store.ts index e798cc207..5a721662e 100644 --- a/report-viewer/src/stores/store.ts +++ b/report-viewer/src/stores/store.ts @@ -2,6 +2,7 @@ import { defineStore } from 'pinia' import type { State, UIState } from './state' import { MetricType } from '@/model/MetricType' import type { SubmissionFile, File } from '@/model/File' +import { FileSortingOptions } from '@/model/ui/FileSortingOptions' /** * The store is a global state management system. It is used to store the state of the application. @@ -31,7 +32,8 @@ const store = defineStore('store', { metric: MetricType.AVERAGE, xScale: 'linear', bucketCount: 10 - } + }, + fileSorting: FileSortingOptions.ALPHABETICAL } }), getters: { diff --git a/report-viewer/src/views/ComparisonView.vue b/report-viewer/src/views/ComparisonView.vue index 996983cfa..e0a0860db 100644 --- a/report-viewer/src/views/ComparisonView.vue +++ b/report-viewer/src/views/ComparisonView.vue @@ -73,6 +73,14 @@ :basecode-in-second="secondBaseCodeMatches" @match-selected="showMatch" /> +
@@ -87,6 +95,7 @@ :highlight-language="language" :base-code-matches="firstBaseCodeMatches" @match-selected="showMatchInSecond" + @files-moved="filesMoved()" class="max-h-0 min-h-full flex-1 overflow-hidden print:max-h-none print:overflow-y-visible" /> @@ -122,6 +132,8 @@ import { MetricType } from '@/model/MetricType' import { Comparison } from '@/model/Comparison' import { redirectOnError } from '@/router' import ToolTipComponent from '@/components/ToolTipComponent.vue' +import { FileSortingOptions, fileSortingTooltips } from '@/model/ui/FileSortingOptions' +import OptionsSelectorComponent from '@/components/optionsSelectors/OptionsSelectorComponent.vue' import type { BaseCodeMatch } from '@/model/BaseCodeReport' library.add(faPrint) @@ -178,6 +190,32 @@ function showMatch(match: Match) { showMatchInSecond(match) } +const sortingOptions = [ + FileSortingOptions.ALPHABETICAL, + FileSortingOptions.MATCH_COVERAGE, + FileSortingOptions.MATCH_COUNT, + FileSortingOptions.MATCH_SIZE +] +const movedAfterSorting = ref(false) +const sortingOptionSelector: Ref = ref(null) + +function changeFileSorting(index: number) { + movedAfterSorting.value = false + if (index < 0) { + return + } + store().uiState.fileSorting = sortingOptions[index] + panel1.value?.sortFiles(store().uiState.fileSorting) + panel2.value?.sortFiles(store().uiState.fileSorting) +} + +function filesMoved() { + movedAfterSorting.value = true + if (sortingOptionSelector.value) { + sortingOptionSelector.value.select(-2) + } +} + function print() { window.print() }