gitlab-ce/app/assets/javascripts/diffs/components/tree_list.vue

246 lines
7.4 KiB
Vue

<script>
import {
GlTooltipDirective,
GlBadge,
GlButtonGroup,
GlButton,
GlSearchBoxByType,
} from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import micromatch from 'micromatch';
import { getModifierKey } from '~/constants';
import { s__, sprintf } from '~/locale';
import { RecycleScroller } from 'vendor/vue-virtual-scroller';
import DiffFileRow from './diff_file_row.vue';
import TreeListHeight from './tree_list_height.vue';
const MODIFIER_KEY = getModifierKey();
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlBadge,
GlButtonGroup,
GlButton,
TreeListHeight,
DiffFileRow,
RecycleScroller,
GlSearchBoxByType,
},
props: {
hideFileStats: {
type: Boolean,
required: true,
},
},
data() {
return {
search: '',
};
},
computed: {
...mapState('diffs', [
'tree',
'renderTreeList',
'currentDiffFileId',
'viewedDiffFileIds',
'realSize',
]),
...mapGetters('diffs', ['allBlobs', 'linkedFile']),
filteredTreeList() {
let search = this.search.toLowerCase().trim();
if (search === '') {
return this.renderTreeList ? this.tree : this.allBlobs;
}
const searchSplit = search.split(',').filter((t) => t);
if (searchSplit.length > 1) {
search = `(${searchSplit.map((s) => s.replace(/(^ +| +$)/g, '')).join('|')})`;
} else {
[search] = searchSplit;
}
return this.allBlobs.reduce((acc, folder) => {
const tree = folder.tree.filter((f) =>
micromatch.contains(f.path, search, { nocase: true }),
);
if (tree.length) {
return acc.concat({
...folder,
tree,
});
}
return acc;
}, []);
},
// Flatten the treeList so there's no nested trees
// This gives us fixed row height for virtual scrolling
// in: [{ path: 'a', tree: [{ path: 'b' }] }, { path: 'c' }]
// out: [{ path: 'a', tree: [{ path: 'b' }] }, { path: 'b' }, { path: 'c' }]
flatFilteredTreeList() {
const result = [];
const createFlatten = (level, hidden) => (item) => {
result.push({
...item,
hidden,
level: item.isHeader ? 0 : level,
key: item.key || item.path,
});
const isHidden = hidden || (item.type === 'tree' && !item.opened);
item.tree.forEach(createFlatten(level + 1, isHidden));
};
this.filteredTreeList.forEach(createFlatten(0));
return result;
},
flatListWithLinkedFile() {
const result = [...this.flatFilteredTreeList];
const linkedFileIndex = result.findIndex((item) => item.path === this.linkedFile.file_path);
const [linkedFileItem] = result.splice(linkedFileIndex, 1);
if (linkedFileItem.parentPath === '/')
return [{ ...linkedFileItem, level: 0, linked: true, hidden: false }, ...result];
// remove detached folder from the tree
const next = result[linkedFileIndex];
const prev = result[linkedFileIndex - 1];
const hasContainingFolder =
prev && prev.type === 'tree' && prev.level === linkedFileItem.level - 1;
const hasSibling = next && next.type !== 'tree' && next.level === linkedFileItem.level;
if (hasContainingFolder && !hasSibling) {
// folder tree is always condensed so we only need to remove the parent folder
result.splice(linkedFileIndex - 1, 1);
}
return [
{
level: 0,
key: 'linked-path',
isHeader: true,
opened: true,
path: linkedFileItem.parentPath,
type: 'tree',
hidden: false,
},
{ ...linkedFileItem, level: 1, linked: true, hidden: false },
...result,
];
},
treeList() {
const list = this.linkedFile ? this.flatListWithLinkedFile : this.flatFilteredTreeList;
if (this.search) return list;
return list.filter((item) => !item.hidden);
},
},
watch: {
currentDiffFileId(hash) {
if (hash) {
this.scrollVirtualScrollerToFileHash(hash);
}
},
},
methods: {
...mapActions('diffs', ['toggleTreeOpen', 'goToFile', 'setRenderTreeList']),
scrollVirtualScrollerToFileHash(hash) {
const index = this.treeList.findIndex((f) => f.fileHash === hash);
if (index !== -1) {
this.$refs.scroller.scrollToItem(index);
}
},
},
searchPlaceholder: sprintf(s__('MergeRequest|Search (e.g. *.vue) (%{MODIFIER_KEY}P)'), {
MODIFIER_KEY,
}),
};
</script>
<template>
<div class="tree-list-holder flex-column gl-flex" data-testid="file-tree-container">
<div class="gl-mb-3 gl-flex gl-items-center">
<h5 class="gl-my-0 gl-inline-block">{{ __('Files') }}</h5>
<gl-badge class="gl-ml-2" data-testid="file-count">{{ realSize }}</gl-badge>
<gl-button-group class="gl-ml-auto">
<gl-button
v-gl-tooltip.hover
icon="list-bulleted"
:selected="!renderTreeList"
:title="__('List view')"
:aria-label="__('List view')"
data-testid="list-view-toggle"
@click="setRenderTreeList({ renderTreeList: false })"
/>
<gl-button
v-gl-tooltip.hover
icon="file-tree"
:selected="renderTreeList"
:title="__('Tree view')"
:aria-label="__('Tree view')"
data-testid="tree-view-toggle"
@click="setRenderTreeList({ renderTreeList: true })"
/>
</gl-button-group>
</div>
<label for="diff-tree-search" class="sr-only">{{ $options.searchPlaceholder }}</label>
<gl-search-box-by-type
id="diff-tree-search"
v-model="search"
:placeholder="$options.searchPlaceholder"
name="diff-tree-search"
data-testid="diff-tree-search"
:clear-button-title="__('Clear search')"
class="gl-mb-3"
/>
<tree-list-height class="gl-min-h-0 gl-grow" :items-count="treeList.length">
<template #default="{ scrollerHeight, rowHeight }">
<div :class="{ 'tree-list-blobs': !renderTreeList || search }" class="mr-tree-list">
<recycle-scroller
v-if="treeList.length"
ref="scroller"
:style="{ height: `${scrollerHeight}px` }"
:items="treeList"
:item-size="rowHeight"
:buffer="100"
key-field="key"
>
<template #default="{ item }">
<diff-file-row
:file="item"
:level="item.level"
:viewed-files="viewedDiffFileIds"
:hide-file-stats="hideFileStats"
:current-diff-file-id="currentDiffFileId"
:style="{ '--level': item.level }"
:class="{ 'tree-list-parent': item.level > 0 }"
class="gl-relative"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="(path) => goToFile({ path })"
/>
</template>
<template #after>
<div class="tree-list-gutter"></div>
</template>
</recycle-scroller>
<p v-else class="prepend-top-20 append-bottom-20 text-center">
{{ s__('MergeRequest|No files found') }}
</p>
</div>
</template>
</tree-list-height>
</div>
</template>
<style>
.tree-list-blobs .file-row-name {
margin-left: 12px;
}
</style>