Skip to content

Commit 31f934c

Browse files
Add filetree on left of diff view (#21012)
This PR adds a filetree to the left side of the files/diff view. Initially the filetree will not be shown and may be shown via a new "Show file tree" button. Showing and hiding is using the same icon as github. Folders are collapsible. On small devices (max-width 991 PX) the file tree will be hidden. Close #18192 Co-authored-by: wxiaoguang <[email protected]>
1 parent 5257512 commit 31f934c

13 files changed

+590
-158
lines changed

templates/repo/commit_page.tmpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{{template "base/head" .}}
22
<div class="page-content repository diff">
33
{{template "repo/header" .}}
4-
<div class="ui container {{if .IsSplitStyle}}fluid padded{{end}}">
4+
<div class="ui container fluid padded">
55
{{$class := ""}}
66
{{if .Commit.Signature}}
77
{{$class = (printf "%s%s" $class " isSigned")}}

templates/repo/diff/box.tmpl

+140-129
Large diffs are not rendered by default.

templates/repo/diff/compare.tmpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{{template "base/head" .}}
22
<div class="page-content repository diff {{if .PageIsComparePull}}compare pull{{end}}">
33
{{template "repo/header" .}}
4-
<div class="ui container {{if .IsSplitStyle}}fluid padded{{end}}">
4+
<div class="ui container fluid padded">
55

66
<h2 class="ui header">
77
{{if and $.PageIsComparePull $.IsSigned (not .Repository.IsArchived)}}

templates/repo/diff/options_dropdown.tmpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<div class="ui dropdown tiny basic button icon-button tooltip" data-content="{{.locale.Tr "repo.diff.options_button"}}">
22
{{svg "octicon-kebab-horizontal"}}
33
<div class="menu">
4-
<a class="item tiny basic toggle button" data-target="#diff-files">{{.locale.Tr "repo.diff.show_diff_stats"}}</a>
4+
<a class="item tiny basic toggle button" id="show-file-list-btn">{{.locale.Tr "repo.diff.show_diff_stats"}}</a>
55
{{if .Issue.Index}}
66
<a class="item" href="{{$.RepoLink}}/pulls/{{.Issue.Index}}.patch" download="{{.Issue.Index}}.patch">{{.locale.Tr "repo.diff.download_patch"}}</a>
77
<a class="item" href="{{$.RepoLink}}/pulls/{{.Issue.Index}}.diff" download="{{.Issue.Index}}.diff">{{.locale.Tr "repo.diff.download_diff"}}</a>

templates/repo/pulls/files.tmpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<div class="page-content repository view issue pull files diff">
77
{{template "repo/header" .}}
8-
<div class="ui container {{if .IsSplitStyle}}fluid padded{{end}}">
8+
<div class="ui container fluid padded">
99
<div class="navbar">
1010
{{template "repo/issue/navbar" .}}
1111
<div class="ui right">
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<template>
2+
<ol class="diff-detail-box diff-stats m-0" id="diff-files" v-if="fileListIsVisible">
3+
<li v-for="file in files" :key="file.NameHash">
4+
<div class="bold df ac pull-right">
5+
<span v-if="file.IsBin" class="ml-1 mr-3">{{ binaryFileMessage }}</span>
6+
{{ file.IsBin ? '' : file.Addition + file.Deletion }}
7+
<span v-if="!file.IsBin" class="diff-stats-bar tooltip mx-3" :data-content="statisticsMessage.replace('%d', (file.Addition + file.Deletion)).replace('%d', file.Addition).replace('%d', file.Deletion)">
8+
<div class="diff-stats-add-bar" :style="{ 'width': diffStatsWidth(file.Addition, file.Deletion) }" />
9+
</span>
10+
</div>
11+
<!-- todo finish all file status, now modify, add, delete and rename -->
12+
<span :class="['status', diffTypeToString(file.Type), 'tooltip']" :data-content="diffTypeToString(file.Type)" data-position="right center">&nbsp;</span>
13+
<a class="file mono" :href="'#diff-' + file.NameHash">{{ file.Name }}</a>
14+
</li>
15+
<li v-if="isIncomplete" id="diff-too-many-files-stats" class="pt-2">
16+
<span class="file df ac sb">{{ tooManyFilesMessage }}
17+
<a :class="['ui', 'basic', 'tiny', 'button', isLoadingNewData === true ? 'disabled' : '']" id="diff-show-more-files-stats" @click.stop="loadMoreData">{{ showMoreMessage }}</a>
18+
</span>
19+
</li>
20+
</ol>
21+
</template>
22+
23+
<script>
24+
import {initTooltip} from '../modules/tippy.js';
25+
import {doLoadMoreFiles} from '../features/repo-diff.js';
26+
27+
const {pageData} = window.config;
28+
29+
export default {
30+
name: 'DiffFileList',
31+
32+
data: () => {
33+
return pageData.diffFileInfo;
34+
},
35+
36+
watch: {
37+
fileListIsVisible(newValue) {
38+
if (newValue === true) {
39+
this.$nextTick(() => {
40+
for (const el of this.$el.querySelectorAll('.tooltip')) {
41+
initTooltip(el);
42+
}
43+
});
44+
}
45+
}
46+
},
47+
48+
mounted() {
49+
document.getElementById('show-file-list-btn').addEventListener('click', this.toggleFileList);
50+
},
51+
52+
unmounted() {
53+
document.getElementById('show-file-list-btn').removeEventListener('click', this.toggleFileList);
54+
},
55+
56+
methods: {
57+
toggleFileList() {
58+
this.fileListIsVisible = !this.fileListIsVisible;
59+
},
60+
diffTypeToString(pType) {
61+
const diffTypes = {
62+
1: 'add',
63+
2: 'modify',
64+
3: 'del',
65+
4: 'rename',
66+
5: 'copy',
67+
};
68+
return diffTypes[pType];
69+
},
70+
diffStatsWidth(adds, dels) {
71+
return `${adds / (adds + dels) * 100}%`;
72+
},
73+
loadMoreData() {
74+
this.isLoadingNewData = true;
75+
doLoadMoreFiles(this.link, this.diffEnd, () => {
76+
this.isLoadingNewData = false;
77+
});
78+
}
79+
},
80+
};
81+
</script>
+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<template>
2+
<div
3+
v-show="fileTreeIsVisible"
4+
id="diff-file-tree"
5+
class="mr-3 mt-3 diff-detail-box"
6+
>
7+
<!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
8+
<div class="ui list" v-if="fileTreeIsVisible">
9+
<DiffFileTreeItem v-for="item in fileTree" :key="item.name" :item="item" />
10+
</div>
11+
<div v-if="isIncomplete" id="diff-too-many-files-stats" class="pt-2">
12+
<span>{{ tooManyFilesMessage }}</span><a :class="['ui', 'basic', 'tiny', 'button', isLoadingNewData === true ? 'disabled' : '']" id="diff-show-more-files-stats" @click.stop="loadMoreData">{{ showMoreMessage }}</a>
13+
</div>
14+
</div>
15+
</template>
16+
17+
<script>
18+
import DiffFileTreeItem from './DiffFileTreeItem.vue';
19+
import {doLoadMoreFiles} from '../features/repo-diff.js';
20+
21+
const {pageData} = window.config;
22+
const LOCAL_STORAGE_KEY = 'diff_file_tree_visible';
23+
24+
export default {
25+
name: 'DiffFileTree',
26+
components: {DiffFileTreeItem},
27+
28+
data: () => {
29+
const fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) === 'true';
30+
pageData.diffFileInfo.fileTreeIsVisible = fileTreeIsVisible;
31+
return pageData.diffFileInfo;
32+
},
33+
34+
computed: {
35+
fileTree() {
36+
const result = [];
37+
for (const file of this.files) {
38+
// Split file into directories
39+
const splits = file.Name.split('/');
40+
let index = 0;
41+
let parent = null;
42+
let isFile = false;
43+
for (const split of splits) {
44+
index += 1;
45+
// reached the end
46+
if (index === splits.length) {
47+
isFile = true;
48+
}
49+
let newParent = {
50+
name: split,
51+
children: [],
52+
isFile
53+
};
54+
55+
if (isFile === true) {
56+
newParent.file = file;
57+
}
58+
59+
if (parent) {
60+
// check if the folder already exists
61+
const existingFolder = parent.children.find(
62+
(x) => x.name === split
63+
);
64+
if (existingFolder) {
65+
newParent = existingFolder;
66+
} else {
67+
parent.children.push(newParent);
68+
}
69+
} else {
70+
const existingFolder = result.find((x) => x.name === split);
71+
if (existingFolder) {
72+
newParent = existingFolder;
73+
} else {
74+
result.push(newParent);
75+
}
76+
}
77+
parent = newParent;
78+
}
79+
}
80+
const mergeChildIfOnlyOneDir = (entries) => {
81+
for (const entry of entries) {
82+
if (entry.children) {
83+
mergeChildIfOnlyOneDir(entry.children);
84+
}
85+
if (entry.children.length === 1 && entry.children[0].isFile === false) {
86+
// Merge it to the parent
87+
entry.name = `${entry.name}/${entry.children[0].name}`;
88+
entry.children = entry.children[0].children;
89+
}
90+
}
91+
};
92+
// Merge folders with just a folder as children in order to
93+
// reduce the depth of our tree.
94+
mergeChildIfOnlyOneDir(result);
95+
return result;
96+
}
97+
},
98+
99+
mounted() {
100+
// ensure correct buttons when we are mounted to the dom
101+
this.adjustToggleButton(this.fileTreeIsVisible);
102+
document.querySelector('.diff-toggle-file-tree-button').addEventListener('click', this.toggleVisibility);
103+
},
104+
unmounted() {
105+
document.querySelector('.diff-toggle-file-tree-button').removeEventListener('click', this.toggleVisibility);
106+
},
107+
methods: {
108+
toggleVisibility() {
109+
this.updateVisibility(!this.fileTreeIsVisible);
110+
},
111+
updateVisibility(visible) {
112+
this.fileTreeIsVisible = visible;
113+
localStorage.setItem(LOCAL_STORAGE_KEY, this.fileTreeIsVisible);
114+
this.adjustToggleButton(this.fileTreeIsVisible);
115+
},
116+
adjustToggleButton(visible) {
117+
const [toShow, toHide] = document.querySelectorAll('.diff-toggle-file-tree-button .icon');
118+
toShow.classList.toggle('hide', visible); // hide the toShow icon if the tree is visible
119+
toHide.classList.toggle('hide', !visible); // similarly
120+
},
121+
loadMoreData() {
122+
this.isLoadingNewData = true;
123+
doLoadMoreFiles(this.link, this.diffEnd, () => {
124+
this.isLoadingNewData = false;
125+
});
126+
}
127+
},
128+
};
129+
</script>
+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<template>
2+
<div v-show="show">
3+
<div class="item" :class="item.isFile ? 'filewrapper p-1' : ''">
4+
<!-- Files -->
5+
<SvgIcon
6+
v-if="item.isFile"
7+
data-position="right center"
8+
name="octicon-file"
9+
class="svg-icon file"
10+
/>
11+
<a
12+
v-if="item.isFile"
13+
class="file ellipsis"
14+
:href="item.isFile ? '#diff-' + item.file.NameHash : ''"
15+
>{{ item.name }}</a>
16+
<SvgIcon
17+
v-if="item.isFile"
18+
data-position="right center"
19+
:name="getIconForDiffType(item.file.Type)"
20+
:class="['svg-icon', getIconForDiffType(item.file.Type), 'status']"
21+
/>
22+
23+
<!-- Directories -->
24+
<div v-if="!item.isFile" class="directory p-1" @click.stop="handleClick(item.isFile)">
25+
<SvgIcon
26+
class="svg-icon"
27+
:name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'"
28+
/>
29+
<SvgIcon
30+
class="svg-icon directory"
31+
name="octicon-file-directory-fill"
32+
/>
33+
<span class="ellipsis">{{ item.name }}</span>
34+
</div>
35+
<div v-show="!collapsed">
36+
<DiffFileTreeItem v-for="childItem in item.children" :key="childItem.name" :item="childItem" class="list" />
37+
</div>
38+
</div>
39+
</div>
40+
</template>
41+
42+
<script>
43+
import {SvgIcon} from '../svg.js';
44+
45+
export default {
46+
name: 'DiffFileTreeItem',
47+
components: {
48+
SvgIcon,
49+
},
50+
51+
props: {
52+
item: {
53+
type: Object,
54+
required: true
55+
},
56+
show: {
57+
type: Boolean,
58+
required: false,
59+
default: true
60+
}
61+
},
62+
63+
data: () => ({
64+
collapsed: false,
65+
}),
66+
methods: {
67+
handleClick(itemIsFile) {
68+
if (itemIsFile) {
69+
return;
70+
}
71+
this.$set(this, 'collapsed', !this.collapsed);
72+
},
73+
getIconForDiffType(pType) {
74+
const diffTypes = {
75+
1: 'octicon-diff-added',
76+
2: 'octicon-diff-modified',
77+
3: 'octicon-diff-removed',
78+
4: 'octicon-diff-renamed',
79+
5: 'octicon-diff-modified', // there is no octicon for copied, so modified should be ok
80+
};
81+
return diffTypes[pType];
82+
},
83+
},
84+
};
85+
</script>
86+
87+
<style scoped>
88+
span.svg-icon.status {
89+
float: right;
90+
}
91+
span.svg-icon.file {
92+
color: var(--color-secondary-dark-7);
93+
}
94+
95+
span.svg-icon.directory {
96+
color: var(--color-primary);
97+
}
98+
99+
span.svg-icon.octicon-diff-modified {
100+
color: var(--color-yellow);
101+
}
102+
103+
span.svg-icon.octicon-diff-added {
104+
color: var(--color-green);
105+
}
106+
107+
span.svg-icon.octicon-diff-removed {
108+
color: var(--color-red);
109+
}
110+
111+
span.svg-icon.octicon-diff-renamed {
112+
color: var(--color-teal);
113+
}
114+
115+
.item.filewrapper {
116+
display: grid !important;
117+
grid-template-columns: 20px 7fr 1fr;
118+
padding-left: 18px !important;
119+
}
120+
121+
.item.filewrapper:hover {
122+
color: var(--color-text);
123+
background: var(--color-hover);
124+
border-radius: 4px;
125+
}
126+
127+
div.directory {
128+
display: grid;
129+
grid-template-columns: 18px 20px auto;
130+
}
131+
132+
div.directory:hover {
133+
color: var(--color-text);
134+
background: var(--color-hover);
135+
border-radius: 4px;
136+
}
137+
138+
div.list {
139+
padding-bottom: 0 !important;
140+
padding-top: inherit !important;
141+
}
142+
143+
a {
144+
text-decoration: none;
145+
}
146+
147+
a:hover {
148+
text-decoration: none;
149+
}
150+
</style>

0 commit comments

Comments
 (0)