mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-16 21:17:26 +02:00
Implemented filtering pull request diff files by file extension
There is new button in pull request diff template, which opens dropbox containing list of all unique extensions that the diff contains. List is created with checkmarks that can be checked/unchecked to filter out files with specific extension. There is also "select all" and "deselect all" buttons. To apply the change and filter out the files user have to click on "Apply" button. If any extensions were filtered out, then button which opens the dropbox is outlined to signal some files were filtered out.
This commit is contained in:
parent
2633f9677d
commit
2ad19a714b
@ -1777,6 +1777,10 @@
|
||||
"repo.pulls.select_commit_hold_shift_for_range": "Select commit. Hold Shift and click to select a range.",
|
||||
"repo.pulls.review_only_possible_for_full_diff": "Review is only possible when viewing the full diff",
|
||||
"repo.pulls.filter_changes_by_commit": "Filter by commit",
|
||||
"repo.pulls.filter_by_file_extension": "Filter by file extension",
|
||||
"repo.pulls.select_all_file_extensions": "Select all",
|
||||
"repo.pulls.deselect_all_file_extensions": "Deselect all",
|
||||
"repo.pulls.apply_file_extension_filter": "Apply",
|
||||
"repo.pulls.nothing_to_compare": "These branches are equal. There is no need to create a pull request.",
|
||||
"repo.pulls.nothing_to_compare_have_tag": "The selected branches/tags are equal.",
|
||||
"repo.pulls.nothing_to_compare_and_allow_empty_pr": "These branches are equal. This PR will be empty.",
|
||||
|
||||
@ -35,6 +35,12 @@
|
||||
{{template "repo/diff/whitespace_dropdown" .}}
|
||||
{{template "repo/diff/options_dropdown" .}}
|
||||
{{if .PageIsPullFiles}}
|
||||
<div id="diff-extension-filter" data-filter_by_file_extension="{{ctx.Locale.Tr "repo.pulls.filter_by_file_extension"}}" data-select_all="{{ctx.Locale.Tr "repo.pulls.select_all_file_extensions"}}" data-deselect_all="{{ctx.Locale.Tr "repo.pulls.deselect_all_file_extensions"}}" data-apply="{{ctx.Locale.Tr "repo.pulls.apply_file_extension_filter"}}">
|
||||
{{/* the following will be replaced by vue component, but this avoids any loading artifacts till the vue component is initialized */}}
|
||||
<div class="ui jump dropdown tiny basic button custom">
|
||||
{{svg "octicon-filter"}}
|
||||
</div>
|
||||
</div>
|
||||
<div id="diff-commit-select" data-merge-base="{{.MergeBase}}" data-issuelink="{{$.Issue.Link}}" data-queryparams="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated={{$.ShowOutdatedComments}}" data-filter_changes_by_commit="{{ctx.Locale.Tr "repo.pulls.filter_changes_by_commit"}}">
|
||||
{{/* the following will be replaced by vue component, but this avoids any loading artifacts till the vue component is initialized */}}
|
||||
<div class="ui jump dropdown tiny basic button custom">
|
||||
|
||||
242
web_src/js/components/DiffFileExtensionFilter.vue
Normal file
242
web_src/js/components/DiffFileExtensionFilter.vue
Normal file
@ -0,0 +1,242 @@
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import {SvgIcon} from '../svg.ts';
|
||||
import {generateElemId} from '../utils/dom.ts';
|
||||
|
||||
type Extension = {
|
||||
ext: string,
|
||||
checked: boolean,
|
||||
count: number,
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {SvgIcon},
|
||||
data: () => {
|
||||
const el = document.querySelector('#diff-extension-filter')!;
|
||||
return {
|
||||
menuVisible: false,
|
||||
extensions: [] as Array<Extension>,
|
||||
isFiltering: false,
|
||||
locale: {
|
||||
filter_by_file_extension: el.getAttribute('data-filter_by_file_extension'),
|
||||
select_all: el.getAttribute('data-select_all'),
|
||||
deselect_all: el.getAttribute('data-deselect_all'),
|
||||
apply: el.getAttribute('data-apply'),
|
||||
} as Record<string, string>,
|
||||
uniqueIdMenu: generateElemId('diff-extension-filter-menu-'),
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
document.body.addEventListener('click', this.onBodyClick);
|
||||
},
|
||||
unmounted() {
|
||||
document.body.removeEventListener('click', this.onBodyClick);
|
||||
},
|
||||
methods: {
|
||||
onBodyClick(event: MouseEvent) {
|
||||
// close this menu on click outside of this element when the dropdown is currently visible opened
|
||||
if (this.$el.contains(event.target)) return;
|
||||
if (this.menuVisible) {
|
||||
this.toggleMenu();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Extract file extension from filename
|
||||
* Returns the extension with dot (e.g., ".ts", ".go")
|
||||
* Returns "(no extension)" for files without extension
|
||||
*/
|
||||
getExtension(filename: string): string {
|
||||
const lastDot = filename.lastIndexOf('.');
|
||||
if (lastDot === -1 || lastDot === 0) {
|
||||
return '(no extension)';
|
||||
}
|
||||
return filename.substring(lastDot);
|
||||
},
|
||||
/**
|
||||
* Scan all diff-file-box elements and build extension list
|
||||
* Check current visibility state and set checked state accordingly
|
||||
*/
|
||||
scanExtensions() {
|
||||
const extensionMap = new Map<string, {total: number, visible: number}>();
|
||||
const fileBoxes = document.querySelectorAll('#diff-file-boxes .diff-file-box[data-new-filename]');
|
||||
|
||||
// Count extensions and track visibility
|
||||
fileBoxes.forEach((box) => {
|
||||
const filename = (box as HTMLElement).getAttribute('data-new-filename') || '';
|
||||
const ext = this.getExtension(filename);
|
||||
const isHidden = (box as HTMLElement).classList.contains('tw-hidden');
|
||||
|
||||
if (!extensionMap.has(ext)) {
|
||||
extensionMap.set(ext, {total: 0, visible: 0});
|
||||
}
|
||||
const stats = extensionMap.get(ext)!;
|
||||
stats.total += 1;
|
||||
if (!isHidden) {
|
||||
stats.visible += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to array and sort by count descending
|
||||
// checked = true if any files of this extension are visible
|
||||
this.extensions = Array.from(extensionMap.entries())
|
||||
.map(([ext, stats]) => ({
|
||||
ext,
|
||||
checked: stats.visible > 0,
|
||||
count: stats.total,
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
// Update filtering state based on current visibility
|
||||
let hiddenCount = 0;
|
||||
fileBoxes.forEach((box) => {
|
||||
if ((box as HTMLElement).classList.contains('tw-hidden')) {
|
||||
hiddenCount += 1;
|
||||
}
|
||||
});
|
||||
this.isFiltering = hiddenCount > 0;
|
||||
},
|
||||
/**
|
||||
* Open dropdown, rescan extensions
|
||||
*/
|
||||
toggleMenu() {
|
||||
this.menuVisible = !this.menuVisible;
|
||||
if (this.menuVisible) {
|
||||
this.scanExtensions();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Select all extensions
|
||||
*/
|
||||
selectAll() {
|
||||
for (const ext of this.extensions) {
|
||||
ext.checked = true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Deselect all extensions
|
||||
*/
|
||||
deselectAll() {
|
||||
for (const ext of this.extensions) {
|
||||
ext.checked = false;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Apply the filter: hide/show diff-file-box elements based on checked extensions
|
||||
*/
|
||||
applyFilter() {
|
||||
const checkedExtensions = new Set(this.extensions.filter((e) => e.checked).map((e) => e.ext));
|
||||
const fileBoxes = document.querySelectorAll('#diff-file-boxes .diff-file-box[data-new-filename]');
|
||||
let hiddenCount = 0;
|
||||
|
||||
fileBoxes.forEach((box) => {
|
||||
const filename = (box as HTMLElement).getAttribute('data-new-filename') || '';
|
||||
const ext = this.getExtension(filename);
|
||||
const isChecked = checkedExtensions.has(ext);
|
||||
|
||||
if (isChecked) {
|
||||
(box as HTMLElement).classList.remove('tw-hidden');
|
||||
} else {
|
||||
(box as HTMLElement).classList.add('tw-hidden');
|
||||
hiddenCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Update filtering state
|
||||
this.isFiltering = hiddenCount > 0;
|
||||
|
||||
// Close the menu after applying
|
||||
this.menuVisible = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="ui scrolling dropdown custom diff-file-extension-filter">
|
||||
<button
|
||||
ref="expandBtn"
|
||||
class="ui tiny basic button tw-relative"
|
||||
:class="{red: isFiltering}"
|
||||
@click.stop="toggleMenu()"
|
||||
:data-tooltip-content="locale.filter_by_file_extension"
|
||||
aria-haspopup="true"
|
||||
:aria-label="locale.filter_by_file_extension"
|
||||
:aria-controls="uniqueIdMenu"
|
||||
>
|
||||
<svg-icon name="octicon-filter"/>
|
||||
<span v-if="isFiltering" class="filter-indicator-dot"/>
|
||||
</button>
|
||||
<!-- this dropdown is not managed by Fomantic UI, so it needs some classes like "transition" explicitly -->
|
||||
<div class="left menu transition" :id="uniqueIdMenu" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak :aria-expanded="menuVisible ? 'true': 'false'">
|
||||
<div class="ui form">
|
||||
<!-- Extension checkboxes -->
|
||||
<div class="grouped fields">
|
||||
<template v-for="ext in extensions" :key="ext.ext">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
:id="`ext-filter-${ext.ext}`"
|
||||
v-model="ext.checked"
|
||||
/>
|
||||
<label :for="`ext-filter-${ext.ext}`" style="cursor: pointer">
|
||||
<span class="tw-font-mono">{{ ext.ext }}</span>
|
||||
<span class="tw-text-text-light-2">({{ ext.count }})</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Select all / Deselect all buttons -->
|
||||
<div class="ui divider" style="margin: 0.5rem 0"/>
|
||||
<div class="tw-flex tw-gap-2 tw-px-2">
|
||||
<a href="#" class="tw-flex-1" @click.prevent="selectAll()">{{ locale.select_all }}</a>
|
||||
<a href="#" class="tw-flex-1" @click.prevent="deselectAll()">{{ locale.deselect_all }}</a>
|
||||
</div>
|
||||
|
||||
<!-- Apply button -->
|
||||
<div class="ui divider" style="margin: 0.5rem 0"/>
|
||||
<button class="ui button fluid" @click="applyFilter()">
|
||||
{{ locale.apply }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.ui.dropdown.diff-file-extension-filter .menu {
|
||||
margin-top: 0.25em;
|
||||
overflow-x: hidden;
|
||||
max-height: 450px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.ui.dropdown.diff-file-extension-filter .menu .ui.form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ui.dropdown.diff-file-extension-filter .grouped.fields > .field {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.ui.dropdown.diff-file-extension-filter .grouped.fields > .field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ui.dropdown.diff-file-extension-filter .button.red {
|
||||
color: var(--color-red-700);
|
||||
border-color: var(--color-red-300);
|
||||
background: var(--color-red-50);
|
||||
}
|
||||
|
||||
.ui.dropdown.diff-file-extension-filter .filter-indicator-dot {
|
||||
position: absolute;
|
||||
top: 0.15rem;
|
||||
right: 0.15rem;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 9999px;
|
||||
background: var(--color-red-600);
|
||||
box-shadow: 0 0 0 2px var(--color-body);
|
||||
}
|
||||
</style>
|
||||
10
web_src/js/features/repo-diff-extensionfilter.ts
Normal file
10
web_src/js/features/repo-diff-extensionfilter.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import {createApp} from 'vue';
|
||||
import DiffFileExtensionFilter from '../components/DiffFileExtensionFilter.vue';
|
||||
|
||||
export function initDiffFileExtensionFilter() {
|
||||
const el = document.querySelector('#diff-extension-filter');
|
||||
if (!el) return;
|
||||
|
||||
const extensionFilter = createApp(DiffFileExtensionFilter);
|
||||
extensionFilter.mount(el);
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import {initRepoIssueContentHistory} from './repo-issue-content.ts';
|
||||
import {initDiffFileTree} from './repo-diff-filetree.ts';
|
||||
import {initDiffCommitSelect} from './repo-diff-commitselect.ts';
|
||||
import {initDiffFileExtensionFilter} from './repo-diff-extensionfilter.ts';
|
||||
import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.ts';
|
||||
import {initViewedCheckboxListenerFor, initExpandAndCollapseFilesButton} from './pull-view-file.ts';
|
||||
import {initImageDiff} from './imagediff.ts';
|
||||
@ -281,6 +282,7 @@ export function initRepoDiffView() {
|
||||
initRepoDiffConversationNav(); // "previous" and "next" buttons only appear on "diff" page
|
||||
initDiffFileTree();
|
||||
initDiffCommitSelect();
|
||||
initDiffFileExtensionFilter();
|
||||
initRepoDiffShowMore();
|
||||
initDiffHeaderPopup();
|
||||
initViewedCheckboxListenerFor();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user