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 <wxiaoguang@gmail.com>tokarchuk/v1.18
parent
525751243e
commit
31f934c1d8
@ -0,0 +1,81 @@ |
|||||||
|
<template> |
||||||
|
<ol class="diff-detail-box diff-stats m-0" id="diff-files" v-if="fileListIsVisible"> |
||||||
|
<li v-for="file in files" :key="file.NameHash"> |
||||||
|
<div class="bold df ac pull-right"> |
||||||
|
<span v-if="file.IsBin" class="ml-1 mr-3">{{ binaryFileMessage }}</span> |
||||||
|
{{ file.IsBin ? '' : file.Addition + file.Deletion }} |
||||||
|
<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)"> |
||||||
|
<div class="diff-stats-add-bar" :style="{ 'width': diffStatsWidth(file.Addition, file.Deletion) }" /> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<!-- todo finish all file status, now modify, add, delete and rename --> |
||||||
|
<span :class="['status', diffTypeToString(file.Type), 'tooltip']" :data-content="diffTypeToString(file.Type)" data-position="right center"> </span> |
||||||
|
<a class="file mono" :href="'#diff-' + file.NameHash">{{ file.Name }}</a> |
||||||
|
</li> |
||||||
|
<li v-if="isIncomplete" id="diff-too-many-files-stats" class="pt-2"> |
||||||
|
<span class="file df ac sb">{{ tooManyFilesMessage }} |
||||||
|
<a :class="['ui', 'basic', 'tiny', 'button', isLoadingNewData === true ? 'disabled' : '']" id="diff-show-more-files-stats" @click.stop="loadMoreData">{{ showMoreMessage }}</a> |
||||||
|
</span> |
||||||
|
</li> |
||||||
|
</ol> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import {initTooltip} from '../modules/tippy.js'; |
||||||
|
import {doLoadMoreFiles} from '../features/repo-diff.js'; |
||||||
|
|
||||||
|
const {pageData} = window.config; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'DiffFileList', |
||||||
|
|
||||||
|
data: () => { |
||||||
|
return pageData.diffFileInfo; |
||||||
|
}, |
||||||
|
|
||||||
|
watch: { |
||||||
|
fileListIsVisible(newValue) { |
||||||
|
if (newValue === true) { |
||||||
|
this.$nextTick(() => { |
||||||
|
for (const el of this.$el.querySelectorAll('.tooltip')) { |
||||||
|
initTooltip(el); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
mounted() { |
||||||
|
document.getElementById('show-file-list-btn').addEventListener('click', this.toggleFileList); |
||||||
|
}, |
||||||
|
|
||||||
|
unmounted() { |
||||||
|
document.getElementById('show-file-list-btn').removeEventListener('click', this.toggleFileList); |
||||||
|
}, |
||||||
|
|
||||||
|
methods: { |
||||||
|
toggleFileList() { |
||||||
|
this.fileListIsVisible = !this.fileListIsVisible; |
||||||
|
}, |
||||||
|
diffTypeToString(pType) { |
||||||
|
const diffTypes = { |
||||||
|
1: 'add', |
||||||
|
2: 'modify', |
||||||
|
3: 'del', |
||||||
|
4: 'rename', |
||||||
|
5: 'copy', |
||||||
|
}; |
||||||
|
return diffTypes[pType]; |
||||||
|
}, |
||||||
|
diffStatsWidth(adds, dels) { |
||||||
|
return `${adds / (adds + dels) * 100}%`; |
||||||
|
}, |
||||||
|
loadMoreData() { |
||||||
|
this.isLoadingNewData = true; |
||||||
|
doLoadMoreFiles(this.link, this.diffEnd, () => { |
||||||
|
this.isLoadingNewData = false; |
||||||
|
}); |
||||||
|
} |
||||||
|
}, |
||||||
|
}; |
||||||
|
</script> |
@ -0,0 +1,129 @@ |
|||||||
|
<template> |
||||||
|
<div |
||||||
|
v-show="fileTreeIsVisible" |
||||||
|
id="diff-file-tree" |
||||||
|
class="mr-3 mt-3 diff-detail-box" |
||||||
|
> |
||||||
|
<!-- only render the tree if we're visible. in many cases this is something that doesn't change very often --> |
||||||
|
<div class="ui list" v-if="fileTreeIsVisible"> |
||||||
|
<DiffFileTreeItem v-for="item in fileTree" :key="item.name" :item="item" /> |
||||||
|
</div> |
||||||
|
<div v-if="isIncomplete" id="diff-too-many-files-stats" class="pt-2"> |
||||||
|
<span>{{ tooManyFilesMessage }}</span><a :class="['ui', 'basic', 'tiny', 'button', isLoadingNewData === true ? 'disabled' : '']" id="diff-show-more-files-stats" @click.stop="loadMoreData">{{ showMoreMessage }}</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import DiffFileTreeItem from './DiffFileTreeItem.vue'; |
||||||
|
import {doLoadMoreFiles} from '../features/repo-diff.js'; |
||||||
|
|
||||||
|
const {pageData} = window.config; |
||||||
|
const LOCAL_STORAGE_KEY = 'diff_file_tree_visible'; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'DiffFileTree', |
||||||
|
components: {DiffFileTreeItem}, |
||||||
|
|
||||||
|
data: () => { |
||||||
|
const fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) === 'true'; |
||||||
|
pageData.diffFileInfo.fileTreeIsVisible = fileTreeIsVisible; |
||||||
|
return pageData.diffFileInfo; |
||||||
|
}, |
||||||
|
|
||||||
|
computed: { |
||||||
|
fileTree() { |
||||||
|
const result = []; |
||||||
|
for (const file of this.files) { |
||||||
|
// Split file into directories |
||||||
|
const splits = file.Name.split('/'); |
||||||
|
let index = 0; |
||||||
|
let parent = null; |
||||||
|
let isFile = false; |
||||||
|
for (const split of splits) { |
||||||
|
index += 1; |
||||||
|
// reached the end |
||||||
|
if (index === splits.length) { |
||||||
|
isFile = true; |
||||||
|
} |
||||||
|
let newParent = { |
||||||
|
name: split, |
||||||
|
children: [], |
||||||
|
isFile |
||||||
|
}; |
||||||
|
|
||||||
|
if (isFile === true) { |
||||||
|
newParent.file = file; |
||||||
|
} |
||||||
|
|
||||||
|
if (parent) { |
||||||
|
// check if the folder already exists |
||||||
|
const existingFolder = parent.children.find( |
||||||
|
(x) => x.name === split |
||||||
|
); |
||||||
|
if (existingFolder) { |
||||||
|
newParent = existingFolder; |
||||||
|
} else { |
||||||
|
parent.children.push(newParent); |
||||||
|
} |
||||||
|
} else { |
||||||
|
const existingFolder = result.find((x) => x.name === split); |
||||||
|
if (existingFolder) { |
||||||
|
newParent = existingFolder; |
||||||
|
} else { |
||||||
|
result.push(newParent); |
||||||
|
} |
||||||
|
} |
||||||
|
parent = newParent; |
||||||
|
} |
||||||
|
} |
||||||
|
const mergeChildIfOnlyOneDir = (entries) => { |
||||||
|
for (const entry of entries) { |
||||||
|
if (entry.children) { |
||||||
|
mergeChildIfOnlyOneDir(entry.children); |
||||||
|
} |
||||||
|
if (entry.children.length === 1 && entry.children[0].isFile === false) { |
||||||
|
// Merge it to the parent |
||||||
|
entry.name = `${entry.name}/${entry.children[0].name}`; |
||||||
|
entry.children = entry.children[0].children; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
// Merge folders with just a folder as children in order to |
||||||
|
// reduce the depth of our tree. |
||||||
|
mergeChildIfOnlyOneDir(result); |
||||||
|
return result; |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
mounted() { |
||||||
|
// ensure correct buttons when we are mounted to the dom |
||||||
|
this.adjustToggleButton(this.fileTreeIsVisible); |
||||||
|
document.querySelector('.diff-toggle-file-tree-button').addEventListener('click', this.toggleVisibility); |
||||||
|
}, |
||||||
|
unmounted() { |
||||||
|
document.querySelector('.diff-toggle-file-tree-button').removeEventListener('click', this.toggleVisibility); |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
toggleVisibility() { |
||||||
|
this.updateVisibility(!this.fileTreeIsVisible); |
||||||
|
}, |
||||||
|
updateVisibility(visible) { |
||||||
|
this.fileTreeIsVisible = visible; |
||||||
|
localStorage.setItem(LOCAL_STORAGE_KEY, this.fileTreeIsVisible); |
||||||
|
this.adjustToggleButton(this.fileTreeIsVisible); |
||||||
|
}, |
||||||
|
adjustToggleButton(visible) { |
||||||
|
const [toShow, toHide] = document.querySelectorAll('.diff-toggle-file-tree-button .icon'); |
||||||
|
toShow.classList.toggle('hide', visible); // hide the toShow icon if the tree is visible |
||||||
|
toHide.classList.toggle('hide', !visible); // similarly |
||||||
|
}, |
||||||
|
loadMoreData() { |
||||||
|
this.isLoadingNewData = true; |
||||||
|
doLoadMoreFiles(this.link, this.diffEnd, () => { |
||||||
|
this.isLoadingNewData = false; |
||||||
|
}); |
||||||
|
} |
||||||
|
}, |
||||||
|
}; |
||||||
|
</script> |
@ -0,0 +1,150 @@ |
|||||||
|
<template> |
||||||
|
<div v-show="show"> |
||||||
|
<div class="item" :class="item.isFile ? 'filewrapper p-1' : ''"> |
||||||
|
<!-- Files --> |
||||||
|
<SvgIcon |
||||||
|
v-if="item.isFile" |
||||||
|
data-position="right center" |
||||||
|
name="octicon-file" |
||||||
|
class="svg-icon file" |
||||||
|
/> |
||||||
|
<a |
||||||
|
v-if="item.isFile" |
||||||
|
class="file ellipsis" |
||||||
|
:href="item.isFile ? '#diff-' + item.file.NameHash : ''" |
||||||
|
>{{ item.name }}</a> |
||||||
|
<SvgIcon |
||||||
|
v-if="item.isFile" |
||||||
|
data-position="right center" |
||||||
|
:name="getIconForDiffType(item.file.Type)" |
||||||
|
:class="['svg-icon', getIconForDiffType(item.file.Type), 'status']" |
||||||
|
/> |
||||||
|
|
||||||
|
<!-- Directories --> |
||||||
|
<div v-if="!item.isFile" class="directory p-1" @click.stop="handleClick(item.isFile)"> |
||||||
|
<SvgIcon |
||||||
|
class="svg-icon" |
||||||
|
:name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" |
||||||
|
/> |
||||||
|
<SvgIcon |
||||||
|
class="svg-icon directory" |
||||||
|
name="octicon-file-directory-fill" |
||||||
|
/> |
||||||
|
<span class="ellipsis">{{ item.name }}</span> |
||||||
|
</div> |
||||||
|
<div v-show="!collapsed"> |
||||||
|
<DiffFileTreeItem v-for="childItem in item.children" :key="childItem.name" :item="childItem" class="list" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import {SvgIcon} from '../svg.js'; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'DiffFileTreeItem', |
||||||
|
components: { |
||||||
|
SvgIcon, |
||||||
|
}, |
||||||
|
|
||||||
|
props: { |
||||||
|
item: { |
||||||
|
type: Object, |
||||||
|
required: true |
||||||
|
}, |
||||||
|
show: { |
||||||
|
type: Boolean, |
||||||
|
required: false, |
||||||
|
default: true |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
data: () => ({ |
||||||
|
collapsed: false, |
||||||
|
}), |
||||||
|
methods: { |
||||||
|
handleClick(itemIsFile) { |
||||||
|
if (itemIsFile) { |
||||||
|
return; |
||||||
|
} |
||||||
|
this.$set(this, 'collapsed', !this.collapsed); |
||||||
|
}, |
||||||
|
getIconForDiffType(pType) { |
||||||
|
const diffTypes = { |
||||||
|
1: 'octicon-diff-added', |
||||||
|
2: 'octicon-diff-modified', |
||||||
|
3: 'octicon-diff-removed', |
||||||
|
4: 'octicon-diff-renamed', |
||||||
|
5: 'octicon-diff-modified', // there is no octicon for copied, so modified should be ok |
||||||
|
}; |
||||||
|
return diffTypes[pType]; |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
span.svg-icon.status { |
||||||
|
float: right; |
||||||
|
} |
||||||
|
span.svg-icon.file { |
||||||
|
color: var(--color-secondary-dark-7); |
||||||
|
} |
||||||
|
|
||||||
|
span.svg-icon.directory { |
||||||
|
color: var(--color-primary); |
||||||
|
} |
||||||
|
|
||||||
|
span.svg-icon.octicon-diff-modified { |
||||||
|
color: var(--color-yellow); |
||||||
|
} |
||||||
|
|
||||||
|
span.svg-icon.octicon-diff-added { |
||||||
|
color: var(--color-green); |
||||||
|
} |
||||||
|
|
||||||
|
span.svg-icon.octicon-diff-removed { |
||||||
|
color: var(--color-red); |
||||||
|
} |
||||||
|
|
||||||
|
span.svg-icon.octicon-diff-renamed { |
||||||
|
color: var(--color-teal); |
||||||
|
} |
||||||
|
|
||||||
|
.item.filewrapper { |
||||||
|
display: grid !important; |
||||||
|
grid-template-columns: 20px 7fr 1fr; |
||||||
|
padding-left: 18px !important; |
||||||
|
} |
||||||
|
|
||||||
|
.item.filewrapper:hover { |
||||||
|
color: var(--color-text); |
||||||
|
background: var(--color-hover); |
||||||
|
border-radius: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
div.directory { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: 18px 20px auto; |
||||||
|
} |
||||||
|
|
||||||
|
div.directory:hover { |
||||||
|
color: var(--color-text); |
||||||
|
background: var(--color-hover); |
||||||
|
border-radius: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
div.list { |
||||||
|
padding-bottom: 0 !important; |
||||||
|
padding-top: inherit !important; |
||||||
|
} |
||||||
|
|
||||||
|
a { |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
|
||||||
|
a:hover { |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,21 @@ |
|||||||
|
import Vue from 'vue'; |
||||||
|
import DiffFileTree from '../components/DiffFileTree.vue'; |
||||||
|
import DiffFileList from '../components/DiffFileList.vue'; |
||||||
|
|
||||||
|
export default function initDiffFileTree() { |
||||||
|
const el = document.getElementById('diff-file-tree-container'); |
||||||
|
if (!el) return; |
||||||
|
|
||||||
|
const View = Vue.extend({ |
||||||
|
render: (createElement) => createElement(DiffFileTree), |
||||||
|
}); |
||||||
|
new View().$mount(el); |
||||||
|
|
||||||
|
const fileListElement = document.getElementById('diff-file-list-container'); |
||||||
|
if (!fileListElement) return; |
||||||
|
|
||||||
|
const fileListView = Vue.extend({ |
||||||
|
render: (createElement) => createElement(DiffFileList), |
||||||
|
}); |
||||||
|
new fileListView().$mount(fileListElement); |
||||||
|
} |
Loading…
Reference in new issue